1
1
# pylint: disable=too-many-lines
2
2
3
3
import ast
4
+ import base64
4
5
import collections
5
6
import contextlib
6
7
import functools
8
+ import hashlib
7
9
import logging
8
10
import re
9
11
import uuid
@@ -2275,6 +2277,43 @@ def _if_to_py_ast(ctx: GeneratorContext, node: If) -> GeneratedPyAST[ast.expr]:
2275
2277
)
2276
2278
2277
2279
2280
+ _IMPORT_HASH_TRANSLATE_TABLE = str .maketrans ({"=" : "" , "+" : "" , "/" : "" })
2281
+
2282
+
2283
+ @functools .lru_cache
2284
+ def _import_hash (s : str ) -> str :
2285
+ """Generate a short, consistent, hash which can be appended to imported module
2286
+ names to effectively separate them from objects of the same name defined in the
2287
+ module.
2288
+
2289
+ Aliases in Clojure exist in a separate "namespace" from interned values, but
2290
+ Basilisp generates Python modules (which are essentially just a single shared
2291
+ namespace), so it is possible that imported module names could clash with `def`'ed
2292
+ names.
2293
+
2294
+ Below, we generate a truncated URL-safe Base64 representation of the MD5 hash of
2295
+ the input string (typically the first '.' delimited component of the potentially
2296
+ qualified module name), removing any '-' characters since those are not safe for
2297
+ Python identifiers.
2298
+
2299
+ The hash doesn't need to be cryptographically secure, but it does need to be
2300
+ consistent across sessions such that when cached namespace modules are reloaded,
2301
+ the new session can find objects generated by the session which generated the
2302
+ cache file. Since we are not concerned with being able to round-trip this data,
2303
+ destructive modifications are not an issue."""
2304
+ digest = hashlib .md5 (s .encode ()).digest ()
2305
+ return base64 .b64encode (digest ).decode ().translate (_IMPORT_HASH_TRANSLATE_TABLE )[:6 ]
2306
+
2307
+
2308
+ def _import_name (root : str , * submodules : str ) -> Tuple [str , str ]:
2309
+ """Return a tuple of the root import name (with hash suffix) for an import and the
2310
+ full import name (if submodules are provided)."""
2311
+ safe_root = f"{ root } _{ _import_hash (root )} "
2312
+ if not submodules :
2313
+ return safe_root , safe_root
2314
+ return safe_root , "." .join ([safe_root , * submodules ])
2315
+
2316
+
2278
2317
@_with_ast_loc_deps
2279
2318
def _import_to_py_ast (ctx : GeneratorContext , node : Import ) -> GeneratedPyAST [ast .expr ]:
2280
2319
"""Return a Python AST node for a Basilisp `import*` expression."""
@@ -2290,10 +2329,11 @@ def _import_to_py_ast(ctx: GeneratorContext, node: Import) -> GeneratedPyAST[ast
2290
2329
# import if parent and child are both imported:
2291
2330
# (import* collections collections.abc)
2292
2331
if alias .alias is not None :
2293
- py_import_alias = munge (alias .alias )
2332
+ py_import_alias , _ = _import_name ( munge (alias .alias ) )
2294
2333
import_func = _IMPORTLIB_IMPORT_MODULE_FN_NAME
2295
2334
else :
2296
- py_import_alias = safe_name .split ("." , maxsplit = 1 )[0 ]
2335
+ py_import_alias , * submodules = safe_name .split ("." , maxsplit = 1 )
2336
+ py_import_alias , _ = _import_name (py_import_alias , * submodules )
2297
2337
import_func = _BUILTINS_IMPORT_FN_NAME
2298
2338
2299
2339
ctx .symbol_table .context_boundary .new_symbol (
@@ -3268,26 +3308,45 @@ def _interop_prop_to_py_ast(
3268
3308
3269
3309
@_with_ast_loc
3270
3310
def _maybe_class_to_py_ast (
3271
- _ : GeneratorContext , node : MaybeClass
3311
+ ctx : GeneratorContext , node : MaybeClass
3272
3312
) -> GeneratedPyAST [ast .expr ]:
3273
- """Generate a Python AST node for accessing a potential Python module
3274
- variable name."""
3313
+ """Generate a Python AST node for accessing a potential Python module variable
3314
+ name."""
3275
3315
assert node .op == NodeOp .MAYBE_CLASS
3276
- return GeneratedPyAST (
3277
- node = ast .Name (id = _MODULE_ALIASES .get (node .class_ , node .class_ ), ctx = ast .Load ())
3278
- )
3316
+ if (mod_name := _MODULE_ALIASES .get (node .class_ )) is None :
3317
+ current_ns = ctx .current_ns
3318
+
3319
+ # For imported modules only, we should generate the name reference using a
3320
+ # unique, consistent hash name (just as they are imported) to avoid clashing
3321
+ # with names def'ed later in the namespace.
3322
+ name = sym .symbol (node .form .name )
3323
+ if (alias := current_ns .import_aliases .val_at (name )) is not None :
3324
+ _ , mod_name = _import_name (munge (alias .name ))
3325
+ elif name in current_ns .imports :
3326
+ root , * submodules = node .class_ .split ("." , maxsplit = 1 )
3327
+ _ , mod_name = _import_name (root , * submodules )
3328
+
3329
+ # Names which are not module references should be passed through.
3330
+ if mod_name is None :
3331
+ mod_name = node .class_
3332
+
3333
+ return GeneratedPyAST (node = ast .Name (id = mod_name , ctx = ast .Load ()))
3279
3334
3280
3335
3281
3336
@_with_ast_loc
3282
3337
def _maybe_host_form_to_py_ast (
3283
3338
_ : GeneratorContext , node : MaybeHostForm
3284
3339
) -> GeneratedPyAST [ast .expr ]:
3285
- """Generate a Python AST node for accessing a potential Python module
3286
- variable name with a namespace."""
3340
+ """Generate a Python AST node for accessing a potential Python module variable name
3341
+ with a namespace."""
3287
3342
assert node .op == NodeOp .MAYBE_HOST_FORM
3288
- return GeneratedPyAST (
3289
- node = _load_attr (f"{ _MODULE_ALIASES .get (node .class_ , node .class_ )} .{ node .field } " )
3290
- )
3343
+ if (mod_name := _MODULE_ALIASES .get (node .class_ )) is None :
3344
+ # At import time, the compiler generates a unique, consistent name for the root
3345
+ # level Python name to avoid clashing with names later def'ed in the namespace.
3346
+ # This is the same logic applied to completing the reference.
3347
+ root , * submodules = node .class_ .split ("." , maxsplit = 1 )
3348
+ __ , mod_name = _import_name (root , * submodules )
3349
+ return GeneratedPyAST (node = _load_attr (f"{ mod_name } .{ node .field } " ))
3291
3350
3292
3351
3293
3352
#########################
0 commit comments