cleanup, lint, add installation "feature," write readme, add some hardcoded skips for modules that don't like to be treated like files (numpy lookin at ya)
This commit is contained in:
parent
b0996c43eb
commit
453c0dcece
13 changed files with 544 additions and 273 deletions
106
README.md
106
README.md
|
@ -1,5 +1,107 @@
|
|||
# lazy-import
|
||||
# no-more-imports
|
||||
|
||||
python with no imports
|
||||
|
||||
hell ya it's got side effects
|
||||
*"hell ya it's got side effects"*
|
||||
|
||||
## Pitch
|
||||
|
||||
Do YOU have an unbounded hostility for others including your future self?
|
||||
|
||||
Do YOU hate code that can be understood and run?
|
||||
|
||||
Do YOU hate typing the word `import`?
|
||||
|
||||
Then `no-more-imports` might be right for YOU.
|
||||
|
||||
Simply import `no_more_imports`, and you'll never need another import again!
|
||||
|
||||
```python
|
||||
import no_more_imports
|
||||
|
||||
# just use it baby nobody's watching!
|
||||
re.search('hell ya', 'can i get a hell ya')
|
||||
|
||||
def eff_it(x=1):
|
||||
# you bet it'll install packages if you don't have them
|
||||
generator = numpy.random.default_rng()
|
||||
data = generator.random((100, 100))
|
||||
|
||||
# forgot where something comes from? no problem, just live your life
|
||||
data = default_rng().random((10,10))
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```shell
|
||||
pip install no-more-imports
|
||||
```
|
||||
|
||||
## "Features"
|
||||
|
||||
- Import it once in an interpreter session, it'll lazify everything else.
|
||||
That's right - import it at the root of your package and that's a single import for the entire package!
|
||||
- Patch the currently importing module! no need to put code in another module
|
||||
like some [*other*](https://github.com/aroberge/ideas) dynamic AST rewriting packages
|
||||
- You only need to use the fully qualified module name once,
|
||||
afterwards, just use the name of the function or class
|
||||
- Dodges your other, regular names and doesn't try to import every variable in the world
|
||||
- Tries to install missing packages if they aren't already! The height of convenience!
|
||||
|
||||
## Usage
|
||||
|
||||
Do not use this package
|
||||
|
||||
## How it Works
|
||||
|
||||
Well buster, i gotta say that's a little nosy, but if you must know,
|
||||
on import, we take over the import system and inject a bunch of code!
|
||||
|
||||
Specifically, we interrupt the part of the import process where the source code is read,
|
||||
and instead of leaving it alone, we fiddle around with it.
|
||||
|
||||
First we parse it into an AST tree and try and find any names that are unbound.
|
||||
We mimic python's scope, so we don't try and import any variables that are actually declared.
|
||||
If we find references to a name that may have already been used before,
|
||||
like `match()` being used after `re.match()`, we stash those as aliases that
|
||||
we need to create.
|
||||
|
||||
After finding names, we generate some frontmatter that gets injected at the start of the file.
|
||||
This handles imports and also assigns the abbreviated name.
|
||||
|
||||
The example above is actually this!
|
||||
|
||||
```python
|
||||
import re
|
||||
import numpy.random
|
||||
default_rng = numpy.random.default_rng
|
||||
import no_more_imports
|
||||
|
||||
# just use it baby nobody's watching!
|
||||
re.search('hell ya', 'can i get a hell ya')
|
||||
|
||||
def eff_it(x=1):
|
||||
# you bet it'll install packages if you don't have them
|
||||
generator = numpy.random.default_rng()
|
||||
data = generator.random((100, 100))
|
||||
|
||||
# forgot where something comes from? no problem, just live your life
|
||||
data = default_rng().random((10,10))
|
||||
```
|
||||
|
||||
To be able to do that in the importing frame, rather than just any imports
|
||||
that happen afterwards, on import we intercept the calling frame,
|
||||
inspect its source code for unbound names,
|
||||
and execute that extra frontmattter segment in the context of the calling frame!
|
||||
|
||||
No fuss! All bugs!
|
||||
|
||||
## Caveats
|
||||
|
||||
- I already told you to not use this package
|
||||
- You can't lazily refer to names in the importing module
|
||||
*at the module level* - the check for unbound names happens before
|
||||
imports happen, so there's nothing we can do.
|
||||
You *can* use unbound names at the module level in any module
|
||||
that's imported *after* the first module that imports `no_more_imports` since
|
||||
after that point we own the import machinery :)
|
||||
|
|
189
pdm.lock
189
pdm.lock
|
@ -2,21 +2,72 @@
|
|||
# It is not intended for manual editing.
|
||||
|
||||
[metadata]
|
||||
groups = ["default", "tests"]
|
||||
groups = ["default", "dev", "tests"]
|
||||
strategy = ["inherit_metadata"]
|
||||
lock_version = "4.5.0"
|
||||
content_hash = "sha256:7fbe8fcf82251321bd2b6ad10d735dd1e657cbf01fed87457fcd3e33a453592a"
|
||||
content_hash = "sha256:48f2f19e324f95afaeb97d64e1d9bc9b95df7986ac7897935eca473952d81844"
|
||||
|
||||
[[metadata.targets]]
|
||||
requires_python = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "24.10.0"
|
||||
requires_python = ">=3.9"
|
||||
summary = "The uncompromising code formatter."
|
||||
groups = ["dev"]
|
||||
dependencies = [
|
||||
"click>=8.0.0",
|
||||
"mypy-extensions>=0.4.3",
|
||||
"packaging>=22.0",
|
||||
"pathspec>=0.9.0",
|
||||
"platformdirs>=2",
|
||||
"tomli>=1.1.0; python_version < \"3.11\"",
|
||||
"typing-extensions>=4.0.1; python_version < \"3.11\"",
|
||||
]
|
||||
files = [
|
||||
{file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"},
|
||||
{file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"},
|
||||
{file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"},
|
||||
{file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"},
|
||||
{file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"},
|
||||
{file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"},
|
||||
{file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"},
|
||||
{file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"},
|
||||
{file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"},
|
||||
{file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"},
|
||||
{file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"},
|
||||
{file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"},
|
||||
{file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"},
|
||||
{file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"},
|
||||
{file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"},
|
||||
{file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"},
|
||||
{file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"},
|
||||
{file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.7"
|
||||
requires_python = ">=3.7"
|
||||
summary = "Composable command line interface toolkit"
|
||||
groups = ["dev"]
|
||||
dependencies = [
|
||||
"colorama; platform_system == \"Windows\"",
|
||||
"importlib-metadata; python_version < \"3.8\"",
|
||||
]
|
||||
files = [
|
||||
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
||||
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
summary = "Cross-platform colored terminal text."
|
||||
groups = ["tests"]
|
||||
marker = "sys_platform == \"win32\""
|
||||
groups = ["dev", "tests"]
|
||||
marker = "sys_platform == \"win32\" or platform_system == \"Windows\""
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
|
@ -46,65 +97,14 @@ files = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.1.2"
|
||||
requires_python = ">=3.10"
|
||||
summary = "Fundamental package for array computing in Python"
|
||||
groups = ["tests"]
|
||||
name = "mypy-extensions"
|
||||
version = "1.0.0"
|
||||
requires_python = ">=3.5"
|
||||
summary = "Type system extensions for programs checked with the mypy type checker."
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "numpy-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30d53720b726ec36a7f88dc873f0eec8447fbc93d93a8f079dfac2629598d6ee"},
|
||||
{file = "numpy-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8d3ca0a72dd8846eb6f7dfe8f19088060fcb76931ed592d29128e0219652884"},
|
||||
{file = "numpy-2.1.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:fc44e3c68ff00fd991b59092a54350e6e4911152682b4782f68070985aa9e648"},
|
||||
{file = "numpy-2.1.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:7c1c60328bd964b53f8b835df69ae8198659e2b9302ff9ebb7de4e5a5994db3d"},
|
||||
{file = "numpy-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cdb606a7478f9ad91c6283e238544451e3a95f30fb5467fbf715964341a8a86"},
|
||||
{file = "numpy-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d666cb72687559689e9906197e3bec7b736764df6a2e58ee265e360663e9baf7"},
|
||||
{file = "numpy-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c6eef7a2dbd0abfb0d9eaf78b73017dbfd0b54051102ff4e6a7b2980d5ac1a03"},
|
||||
{file = "numpy-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:12edb90831ff481f7ef5f6bc6431a9d74dc0e5ff401559a71e5e4611d4f2d466"},
|
||||
{file = "numpy-2.1.2-cp310-cp310-win32.whl", hash = "sha256:a65acfdb9c6ebb8368490dbafe83c03c7e277b37e6857f0caeadbbc56e12f4fb"},
|
||||
{file = "numpy-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:860ec6e63e2c5c2ee5e9121808145c7bf86c96cca9ad396c0bd3e0f2798ccbe2"},
|
||||
{file = "numpy-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b42a1a511c81cc78cbc4539675713bbcf9d9c3913386243ceff0e9429ca892fe"},
|
||||
{file = "numpy-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:faa88bc527d0f097abdc2c663cddf37c05a1c2f113716601555249805cf573f1"},
|
||||
{file = "numpy-2.1.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:c82af4b2ddd2ee72d1fc0c6695048d457e00b3582ccde72d8a1c991b808bb20f"},
|
||||
{file = "numpy-2.1.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:13602b3174432a35b16c4cfb5de9a12d229727c3dd47a6ce35111f2ebdf66ff4"},
|
||||
{file = "numpy-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ebec5fd716c5a5b3d8dfcc439be82a8407b7b24b230d0ad28a81b61c2f4659a"},
|
||||
{file = "numpy-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2b49c3c0804e8ecb05d59af8386ec2f74877f7ca8fd9c1e00be2672e4d399b1"},
|
||||
{file = "numpy-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2cbba4b30bf31ddbe97f1c7205ef976909a93a66bb1583e983adbd155ba72ac2"},
|
||||
{file = "numpy-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8e00ea6fc82e8a804433d3e9cedaa1051a1422cb6e443011590c14d2dea59146"},
|
||||
{file = "numpy-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5006b13a06e0b38d561fab5ccc37581f23c9511879be7693bd33c7cd15ca227c"},
|
||||
{file = "numpy-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:f1eb068ead09f4994dec71c24b2844f1e4e4e013b9629f812f292f04bd1510d9"},
|
||||
{file = "numpy-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7bf0a4f9f15b32b5ba53147369e94296f5fffb783db5aacc1be15b4bf72f43b"},
|
||||
{file = "numpy-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b1d0fcae4f0949f215d4632be684a539859b295e2d0cb14f78ec231915d644db"},
|
||||
{file = "numpy-2.1.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f751ed0a2f250541e19dfca9f1eafa31a392c71c832b6bb9e113b10d050cb0f1"},
|
||||
{file = "numpy-2.1.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:bd33f82e95ba7ad632bc57837ee99dba3d7e006536200c4e9124089e1bf42426"},
|
||||
{file = "numpy-2.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b8cde4f11f0a975d1fd59373b32e2f5a562ade7cde4f85b7137f3de8fbb29a0"},
|
||||
{file = "numpy-2.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d95f286b8244b3649b477ac066c6906fbb2905f8ac19b170e2175d3d799f4df"},
|
||||
{file = "numpy-2.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ab4754d432e3ac42d33a269c8567413bdb541689b02d93788af4131018cbf366"},
|
||||
{file = "numpy-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e585c8ae871fd38ac50598f4763d73ec5497b0de9a0ab4ef5b69f01c6a046142"},
|
||||
{file = "numpy-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9c6c754df29ce6a89ed23afb25550d1c2d5fdb9901d9c67a16e0b16eaf7e2550"},
|
||||
{file = "numpy-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:456e3b11cb79ac9946c822a56346ec80275eaf2950314b249b512896c0d2505e"},
|
||||
{file = "numpy-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a84498e0d0a1174f2b3ed769b67b656aa5460c92c9554039e11f20a05650f00d"},
|
||||
{file = "numpy-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4d6ec0d4222e8ffdab1744da2560f07856421b367928026fb540e1945f2eeeaf"},
|
||||
{file = "numpy-2.1.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:259ec80d54999cc34cd1eb8ded513cb053c3bf4829152a2e00de2371bd406f5e"},
|
||||
{file = "numpy-2.1.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:675c741d4739af2dc20cd6c6a5c4b7355c728167845e3c6b0e824e4e5d36a6c3"},
|
||||
{file = "numpy-2.1.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b2d4e667895cc55e3ff2b56077e4c8a5604361fc21a042845ea3ad67465aa8"},
|
||||
{file = "numpy-2.1.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43cca367bf94a14aca50b89e9bc2061683116cfe864e56740e083392f533ce7a"},
|
||||
{file = "numpy-2.1.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:76322dcdb16fccf2ac56f99048af32259dcc488d9b7e25b51e5eca5147a3fb98"},
|
||||
{file = "numpy-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:32e16a03138cabe0cb28e1007ee82264296ac0983714094380b408097a418cfe"},
|
||||
{file = "numpy-2.1.2-cp313-cp313-win32.whl", hash = "sha256:242b39d00e4944431a3cd2db2f5377e15b5785920421993770cddb89992c3f3a"},
|
||||
{file = "numpy-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f2ded8d9b6f68cc26f8425eda5d3877b47343e68ca23d0d0846f4d312ecaa445"},
|
||||
{file = "numpy-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ffef621c14ebb0188a8633348504a35c13680d6da93ab5cb86f4e54b7e922b5"},
|
||||
{file = "numpy-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad369ed238b1959dfbade9018a740fb9392c5ac4f9b5173f420bd4f37ba1f7a0"},
|
||||
{file = "numpy-2.1.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d82075752f40c0ddf57e6e02673a17f6cb0f8eb3f587f63ca1eaab5594da5b17"},
|
||||
{file = "numpy-2.1.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:1600068c262af1ca9580a527d43dc9d959b0b1d8e56f8a05d830eea39b7c8af6"},
|
||||
{file = "numpy-2.1.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a26ae94658d3ba3781d5e103ac07a876b3e9b29db53f68ed7df432fd033358a8"},
|
||||
{file = "numpy-2.1.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13311c2db4c5f7609b462bc0f43d3c465424d25c626d95040f073e30f7570e35"},
|
||||
{file = "numpy-2.1.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:2abbf905a0b568706391ec6fa15161fad0fb5d8b68d73c461b3c1bab6064dd62"},
|
||||
{file = "numpy-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ef444c57d664d35cac4e18c298c47d7b504c66b17c2ea91312e979fcfbdfb08a"},
|
||||
{file = "numpy-2.1.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:bdd407c40483463898b84490770199d5714dcc9dd9b792f6c6caccc523c00952"},
|
||||
{file = "numpy-2.1.2-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:da65fb46d4cbb75cb417cddf6ba5e7582eb7bb0b47db4b99c9fe5787ce5d91f5"},
|
||||
{file = "numpy-2.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c193d0b0238638e6fc5f10f1b074a6993cb13b0b431f64079a509d63d3aa8b7"},
|
||||
{file = "numpy-2.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a7d80b2e904faa63068ead63107189164ca443b42dd1930299e0d1cb041cec2e"},
|
||||
{file = "numpy-2.1.2.tar.gz", hash = "sha256:13532a088217fa624c99b843eeb54640de23b3414b14aa66d023805eb731066c"},
|
||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -112,12 +112,34 @@ name = "packaging"
|
|||
version = "24.1"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Core utilities for Python packages"
|
||||
groups = ["tests"]
|
||||
groups = ["dev", "tests"]
|
||||
files = [
|
||||
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
|
||||
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.12.1"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Utility library for gitignore style pattern matching of file paths."
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
||||
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.3.6"
|
||||
requires_python = ">=3.8"
|
||||
summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
|
||||
{file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.5.0"
|
||||
|
@ -148,14 +170,53 @@ files = [
|
|||
{file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.6.9"
|
||||
requires_python = ">=3.7"
|
||||
summary = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"},
|
||||
{file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"},
|
||||
{file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"},
|
||||
{file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"},
|
||||
{file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"},
|
||||
{file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"},
|
||||
{file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"},
|
||||
{file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"},
|
||||
{file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"},
|
||||
{file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"},
|
||||
{file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.2"
|
||||
requires_python = ">=3.8"
|
||||
summary = "A lil' TOML parser"
|
||||
groups = ["tests"]
|
||||
groups = ["dev", "tests"]
|
||||
marker = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"},
|
||||
{file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
groups = ["dev"]
|
||||
marker = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||
]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[project]
|
||||
name = "lazy-import"
|
||||
name = "no-more-imports"
|
||||
version = "0.1.0"
|
||||
description = "python with no imports"
|
||||
authors = [
|
||||
|
@ -10,11 +10,40 @@ requires-python = ">=3.10"
|
|||
readme = "README.md"
|
||||
license = {text = "EUPL-1.2"}
|
||||
|
||||
[project.urls]
|
||||
repository = "https://git.jon-e.net/jonny/no-more-imports"
|
||||
homepage = "https://git.jon-e.net/jonny/no-more-imports"
|
||||
issues = "https://git.jon-e.net/jonny/no-more-imports"
|
||||
documentation = "https://git.jon-e.net/jonny/no-more-imports/src/branch/main/README.md"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tests = [
|
||||
"pytest>=8.3.3",
|
||||
"numpy>=2.1.2",
|
||||
]
|
||||
dev = [
|
||||
"black>=24.10.0",
|
||||
"ruff>=0.6.9",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py310"
|
||||
include = ["src/**/*.py"]
|
||||
line-length = 100
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E",
|
||||
"F",
|
||||
"I",
|
||||
]
|
||||
fixable = ["ALL"]
|
||||
|
||||
[tool.black]
|
||||
target-version = ['py310', 'py311', 'py312']
|
||||
enable-unstable-feature = ["string_processing"]
|
||||
preview = true
|
||||
line-length = 100
|
||||
|
||||
[build-system]
|
||||
requires = ["pdm-backend"]
|
||||
build-backend = "pdm.backend"
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
from lazy_import.importer import install
|
||||
install()
|
|
@ -1,104 +0,0 @@
|
|||
import inspect
|
||||
import pdb
|
||||
import sys
|
||||
import ast
|
||||
from typing import Optional
|
||||
from types import ModuleType
|
||||
import os
|
||||
from importlib.abc import MetaPathFinder, Loader, SourceLoader, FileLoader
|
||||
from importlib.machinery import FileFinder
|
||||
from importlib import invalidate_caches
|
||||
from importlib.machinery import ModuleSpec, SourceFileLoader
|
||||
import importlib.util
|
||||
from importlib.util import spec_from_file_location
|
||||
|
||||
|
||||
from lazy_import.ast import NameVisitor, generate_frontmatter
|
||||
|
||||
|
||||
class LazyLoader(FileLoader, SourceLoader):
|
||||
"""
|
||||
Try to import any names that are referenced without imports
|
||||
|
||||
Thx to https://stackoverflow.com/a/43573798/13113166 for the clear example
|
||||
"""
|
||||
|
||||
def get_data(self, path) -> str | None:
|
||||
"""
|
||||
Modify the source code to include imports and assignments to make
|
||||
lazy imports work.
|
||||
|
||||
Do it this way rather than using `source_to_code` because
|
||||
this way we still get meaningful error messages that can show
|
||||
the source lines that are failing
|
||||
"""
|
||||
with open(path) as f:
|
||||
data = f.read()
|
||||
|
||||
if self.name.split('.')[0] in sys.stdlib_module_names:
|
||||
return data
|
||||
|
||||
parsed = ast.parse(data)
|
||||
frontmatter = generate_frontmatter(parsed)
|
||||
|
||||
# put the frontmatter first and replace
|
||||
frontmatter.extend(parsed.body)
|
||||
parsed.body = frontmatter
|
||||
|
||||
# fix after modifying and return to string
|
||||
parsed = ast.fix_missing_locations(parsed)
|
||||
deparsed = ast.unparse(parsed)
|
||||
return deparsed
|
||||
|
||||
class LazyFinder(MetaPathFinder):
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
if path is None or path == "":
|
||||
path = [os.getcwd()] # top level import --
|
||||
|
||||
if fullname.split('.')[0] in sys.builtin_module_names:
|
||||
return None
|
||||
|
||||
if "." in fullname:
|
||||
*parents, name = fullname.split(".")
|
||||
else:
|
||||
name = fullname
|
||||
|
||||
for entry in path:
|
||||
if os.path.isdir(os.path.join(entry, name)):
|
||||
# this module has child modules
|
||||
filename = os.path.join(entry, name, "__init__.py")
|
||||
submodule_locations = [os.path.join(entry, name)]
|
||||
else:
|
||||
filename = os.path.join(entry, name + ".py")
|
||||
submodule_locations = None
|
||||
if not os.path.exists(filename):
|
||||
continue
|
||||
|
||||
return spec_from_file_location(fullname, filename, loader=LazyLoader(fullname, filename),
|
||||
submodule_search_locations=submodule_locations)
|
||||
|
||||
return None # we don't know how to import this
|
||||
|
||||
def patch_importing_frame():
|
||||
"""
|
||||
Inject needed imports into the importing frame as well :)
|
||||
"""
|
||||
current_frame = inspect.currentframe()
|
||||
outer_frames = inspect.getouterframes(current_frame, context=3)
|
||||
importing_frame = outer_frames[-1].frame
|
||||
|
||||
try:
|
||||
source = inspect.getsource(importing_frame)
|
||||
except OSError:
|
||||
# stdin, compiled extensions, etc.
|
||||
return
|
||||
|
||||
node = ast.parse(source)
|
||||
frontmatter = generate_frontmatter(node, mode='str')
|
||||
exec(frontmatter, importing_frame.f_globals, importing_frame.f_locals)
|
||||
|
||||
|
||||
|
||||
def install():
|
||||
patch_importing_frame()
|
||||
sys.meta_path.insert(0, LazyFinder())
|
3
src/no_more_imports/__init__.py
Normal file
3
src/no_more_imports/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from no_more_imports.importer import install
|
||||
|
||||
install()
|
|
@ -1,10 +1,9 @@
|
|||
from typing import Literal, overload
|
||||
import ast
|
||||
import pdb
|
||||
from collections.abc import Container
|
||||
from collections import ChainMap
|
||||
from collections.abc import Container
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Iterator, Literal, overload
|
||||
|
||||
|
||||
@dataclass(eq=True)
|
||||
class Name:
|
||||
|
@ -15,9 +14,7 @@ class Name:
|
|||
|
||||
def __contains__(self, item: str):
|
||||
return item in self.aliases or (
|
||||
item == self.module
|
||||
if self.name is None else
|
||||
item in (self.name, self.id)
|
||||
item == self.module if self.name is None else item in (self.name, self.id)
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -25,7 +22,7 @@ class Name:
|
|||
if self.name is None:
|
||||
return self.module
|
||||
else:
|
||||
return '.'.join([self.module, self.name])
|
||||
return ".".join([self.module, self.name])
|
||||
|
||||
@property
|
||||
def parts(self) -> list[str]:
|
||||
|
@ -41,25 +38,26 @@ class Name:
|
|||
- `module.submodule.subsubmodule.A`
|
||||
|
||||
"""
|
||||
subparts = self.module.split('.')
|
||||
subparts = self.module.split(".")
|
||||
if self.name:
|
||||
subparts.append(self.name)
|
||||
return ['.'.join(subparts[:i+1]) for i in range(len(subparts))]
|
||||
return [".".join(subparts[: i + 1]) for i in range(len(subparts))]
|
||||
|
||||
def in_dict(self, other: Container):
|
||||
return any(part in other for part in self.parts)
|
||||
|
||||
@classmethod
|
||||
def from_ast_name(cls, name: ast.Name) -> 'Name':
|
||||
def from_ast_name(cls, name: ast.Name) -> "Name":
|
||||
return cls.from_str(name.id)
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, name: str) -> 'Name':
|
||||
def from_str(cls, name: str) -> "Name":
|
||||
if len(name_parts := name.rsplit(".", maxsplit=1)) > 1:
|
||||
return Name(module=name_parts[0], name=name_parts[1])
|
||||
else:
|
||||
return Name(module=name)
|
||||
|
||||
|
||||
@dataclass(eq=True)
|
||||
class NameCollection:
|
||||
|
||||
|
@ -81,6 +79,9 @@ class NameCollection:
|
|||
|
||||
self.names.append(new_name)
|
||||
|
||||
def __iter__(self) -> Iterator[Name]:
|
||||
yield from self.names
|
||||
|
||||
|
||||
class NameVisitor(ast.NodeVisitor):
|
||||
"""
|
||||
|
@ -88,7 +89,7 @@ class NameVisitor(ast.NodeVisitor):
|
|||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.real_names = ChainMap({'self': None}, globals()['__builtins__'])
|
||||
self.real_names = ChainMap({"self": None}, globals()["__builtins__"])
|
||||
self.fake_names = NameCollection()
|
||||
|
||||
def pop_ctx(self):
|
||||
|
@ -109,8 +110,6 @@ class NameVisitor(ast.NodeVisitor):
|
|||
"""Add to names"""
|
||||
return self.visit_Import(node)
|
||||
|
||||
|
||||
|
||||
def visit_Name(self, node: ast.Name):
|
||||
"""Either add to real names or fake names depending on ctx"""
|
||||
# print(ast.dump(node))
|
||||
|
@ -124,7 +123,7 @@ class NameVisitor(ast.NodeVisitor):
|
|||
del self.real_names[node.id]
|
||||
else: # pragma: no cover
|
||||
if type(node.ctx) not in (ast.Del, ast.Store, ast.Load):
|
||||
raise ValueError(f'How did this happen!? wrong node ctx type? {node.ctx}')
|
||||
raise ValueError(f"How did this happen!? wrong node ctx type? {node.ctx}")
|
||||
|
||||
def visit_Attribute(self, node: ast.Attribute):
|
||||
# print(ast.dump(node))
|
||||
|
@ -142,17 +141,17 @@ class NameVisitor(ast.NodeVisitor):
|
|||
del self.real_names[attr_name]
|
||||
else: # pragma: no cover
|
||||
if type(node.ctx) not in (ast.Del, ast.Store, ast.Load):
|
||||
raise ValueError(f'How did this happen!? wrong node ctx type? {node.ctx}')
|
||||
raise ValueError(f"How did this happen!? wrong node ctx type? {node.ctx}")
|
||||
|
||||
def visit_FunctionDef(self, node: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda):
|
||||
"""push context"""
|
||||
# print(ast.dump(node))
|
||||
|
||||
# names that should be defined in the parent scope
|
||||
if hasattr(node, 'returns') and node.returns:
|
||||
if hasattr(node, "returns") and node.returns:
|
||||
self._handle_annotation(node.returns)
|
||||
|
||||
if hasattr(node, 'name'):
|
||||
if hasattr(node, "name"):
|
||||
self.real_names[node.name] = node
|
||||
|
||||
# enter function scope
|
||||
|
@ -162,9 +161,9 @@ class NameVisitor(ast.NodeVisitor):
|
|||
self.real_names[arg.arg] = arg
|
||||
if arg.annotation:
|
||||
self._handle_annotation(arg.annotation)
|
||||
if hasattr(args, 'vararg') and args.vararg:
|
||||
if hasattr(args, "vararg") and args.vararg:
|
||||
self.real_names[args.vararg.arg] = args.vararg
|
||||
if hasattr(args, 'kwarg') and args.kwarg:
|
||||
if hasattr(args, "kwarg") and args.kwarg:
|
||||
self.real_names[args.kwarg.arg] = args.kwarg
|
||||
|
||||
self.generic_visit(node)
|
||||
|
@ -213,10 +212,10 @@ class NameVisitor(ast.NodeVisitor):
|
|||
self.real_names[gen.target.id] = gen.target
|
||||
self.generic_visit(node)
|
||||
self.pop_ctx()
|
||||
# if isinstance(gen.iter, ast.Name):
|
||||
# self.visit_Name(gen.iter)
|
||||
# elif isinstance(gen.iter, ast.Call):
|
||||
# self.visit_Attribute(gen.iter.func)
|
||||
# if isinstance(gen.iter, ast.Name):
|
||||
# self.visit_Name(gen.iter)
|
||||
# elif isinstance(gen.iter, ast.Call):
|
||||
# self.visit_Attribute(gen.iter.func)
|
||||
|
||||
def visit_DictComp(self, node: ast.DictComp):
|
||||
self.visit_ListComp(node)
|
||||
|
@ -250,56 +249,76 @@ class NameVisitor(ast.NodeVisitor):
|
|||
After visiting, we remove top-level module level definitions from
|
||||
fake names, since it's possible to refer to things out of order in scopes
|
||||
"""
|
||||
self.fake_names.names = [n for n in self.fake_names.names if n.module not in self.real_names]
|
||||
self.fake_names.names = [
|
||||
n for n in self.fake_names.names if n.module not in self.real_names
|
||||
]
|
||||
|
||||
|
||||
@overload
|
||||
def generate_frontmatter(node: ast.AST, mode: Literal['ast'] = 'ast') -> list[ast.Import | ast.Assign]: ...
|
||||
|
||||
@overload
|
||||
def generate_frontmatter(node: ast.AST, mode: Literal['str'] = 'str') -> str: ...
|
||||
|
||||
|
||||
def generate_frontmatter(node: ast.AST, mode: Literal['ast', 'str'] = 'ast') -> list[ast.Import | ast.Assign] | str:
|
||||
def parse_names(node: ast.AST) -> NameCollection:
|
||||
"""
|
||||
Get the names that need to be imported from an AST module by applying
|
||||
:class:`.NameVisitor`
|
||||
"""
|
||||
visitor = NameVisitor()
|
||||
visitor.visit(node)
|
||||
return visitor.fake_names
|
||||
|
||||
modules = list(dict.fromkeys([name.module for name in visitor.fake_names.names]))
|
||||
|
||||
if mode == 'ast':
|
||||
return _frontmatter_ast(modules, visitor)
|
||||
elif mode == 'str':
|
||||
return _frontmatter_str(modules, visitor)
|
||||
|
||||
@overload
|
||||
def generate_frontmatter(
|
||||
names: NameCollection, mode: Literal["ast"] = "ast"
|
||||
) -> list[ast.Import | ast.Assign]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def generate_frontmatter(names: NameCollection, mode: Literal["str"] = "str") -> str: ...
|
||||
|
||||
|
||||
def generate_frontmatter(
|
||||
names: NameCollection, mode: Literal["ast", "str"] = "ast"
|
||||
) -> list[ast.Import | ast.Assign] | str:
|
||||
|
||||
if mode == "ast":
|
||||
return _frontmatter_ast(names)
|
||||
elif mode == "str":
|
||||
return _frontmatter_str(names)
|
||||
else:
|
||||
raise ValueError("Unknown frontmatter mode")
|
||||
|
||||
|
||||
def _frontmatter_ast(modules: list[str], visitor: NameVisitor) -> list[ast.Import | ast.Assign]:
|
||||
def _frontmatter_ast(names: NameCollection) -> list[ast.Import | ast.Assign]:
|
||||
modules = list(dict.fromkeys([name.module for name in names.names]))
|
||||
|
||||
imports = [ast.Import(names=[ast.alias(name)]) for name in modules]
|
||||
assignments = []
|
||||
for name in visitor.fake_names.names:
|
||||
for name in names.names:
|
||||
for alias in name.aliases:
|
||||
assignments.append(
|
||||
ast.Assign(targets=[ast.Name(id=alias, ctx=ast.Store())],
|
||||
value=ast.Name(id=name.id, ctx=ast.Load()))
|
||||
ast.Assign(
|
||||
targets=[ast.Name(id=alias, ctx=ast.Store())],
|
||||
value=ast.Name(id=name.id, ctx=ast.Load()),
|
||||
)
|
||||
)
|
||||
|
||||
return imports + assignments
|
||||
|
||||
def _frontmatter_str(modules: list[str], visitor: NameVisitor) -> str:
|
||||
imports = [f'import {mod}' for mod in modules]
|
||||
|
||||
def _frontmatter_str(names: NameCollection) -> str:
|
||||
modules = list(dict.fromkeys([name.module for name in names.names]))
|
||||
imports = [f"import {mod}" for mod in modules]
|
||||
|
||||
assignments = []
|
||||
for name in visitor.fake_names.names:
|
||||
for name in names.names:
|
||||
for alias in name.aliases:
|
||||
assignments.append(f'{alias} = {name.id}')
|
||||
|
||||
return '\n'.join(imports + assignments)
|
||||
assignments.append(f"{alias} = {name.id}")
|
||||
|
||||
return "\n".join(imports + assignments)
|
||||
|
||||
|
||||
def flatten_attribute(attr: ast.Attribute) -> str:
|
||||
if isinstance(attr.value, ast.Attribute):
|
||||
return '.'.join([flatten_attribute(attr.value), attr.attr])
|
||||
return ".".join([flatten_attribute(attr.value), attr.attr])
|
||||
elif isinstance(attr.value, ast.Name):
|
||||
return '.'.join([attr.value.id, attr.attr])
|
||||
return ".".join([attr.value.id, attr.attr])
|
||||
elif isinstance(attr.value, ast.Call):
|
||||
return attr.value.func.id
|
6
src/no_more_imports/const.py
Normal file
6
src/no_more_imports/const.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
HARDCODED_SKIPS = [
|
||||
"numpy"
|
||||
]
|
||||
"""
|
||||
Stuff that we don't try and lazify because it's known to be broken
|
||||
"""
|
153
src/no_more_imports/importer.py
Normal file
153
src/no_more_imports/importer.py
Normal file
|
@ -0,0 +1,153 @@
|
|||
import ast
|
||||
import inspect
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from importlib.abc import FileLoader, MetaPathFinder, SourceLoader
|
||||
from importlib.util import find_spec, spec_from_file_location
|
||||
from pprint import pformat
|
||||
|
||||
from no_more_imports.ast import NameCollection, generate_frontmatter, parse_names
|
||||
from no_more_imports.const import HARDCODED_SKIPS
|
||||
|
||||
|
||||
class LazyLoader(FileLoader, SourceLoader):
|
||||
"""
|
||||
Try to import any names that are referenced without imports
|
||||
|
||||
Thx to https://stackoverflow.com/a/43573798/13113166 for the clear example
|
||||
"""
|
||||
|
||||
def get_data(self, path) -> str | None:
|
||||
"""
|
||||
Modify the source code to include imports and assignments to make
|
||||
lazy imports work.
|
||||
|
||||
Do it this way rather than using `source_to_code` because
|
||||
this way we still get meaningful error messages that can show
|
||||
the source lines that are failing
|
||||
"""
|
||||
with open(path) as f:
|
||||
data = f.read()
|
||||
|
||||
base_name = self.name.split(".")[0]
|
||||
if base_name in sys.stdlib_module_names or base_name in HARDCODED_SKIPS:
|
||||
return data
|
||||
|
||||
parsed = ast.parse(data)
|
||||
names = parse_names(parsed)
|
||||
install_packages(names)
|
||||
frontmatter = generate_frontmatter(names)
|
||||
|
||||
# put the frontmatter first and replace
|
||||
frontmatter.extend(parsed.body)
|
||||
parsed.body = frontmatter
|
||||
|
||||
# fix after modifying and return to string
|
||||
parsed = ast.fix_missing_locations(parsed)
|
||||
deparsed = ast.unparse(parsed)
|
||||
return deparsed
|
||||
|
||||
|
||||
class LazyFinder(MetaPathFinder):
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
if path is None or path == "":
|
||||
path = [os.getcwd()] # top level import --
|
||||
|
||||
base_name = fullname.split(".")[0]
|
||||
if base_name in sys.stdlib_module_names or base_name in HARDCODED_SKIPS:
|
||||
return None
|
||||
|
||||
if "." in fullname:
|
||||
*parents, name = fullname.split(".")
|
||||
else:
|
||||
name = fullname
|
||||
|
||||
for entry in path:
|
||||
if os.path.isdir(os.path.join(entry, name)):
|
||||
# this module has child modules
|
||||
filename = os.path.join(entry, name, "__init__.py")
|
||||
submodule_locations = [os.path.join(entry, name)]
|
||||
else:
|
||||
filename = os.path.join(entry, name + ".py")
|
||||
submodule_locations = None
|
||||
if not os.path.exists(filename):
|
||||
continue
|
||||
|
||||
return spec_from_file_location(
|
||||
fullname,
|
||||
filename,
|
||||
loader=LazyLoader(fullname, filename),
|
||||
submodule_search_locations=submodule_locations,
|
||||
)
|
||||
|
||||
return None # we don't know how to import this
|
||||
|
||||
|
||||
def patch_importing_frame():
|
||||
"""
|
||||
Inject needed imports into the importing frame as well :)
|
||||
"""
|
||||
current_frame = inspect.currentframe()
|
||||
outer_frames = inspect.getouterframes(current_frame, context=1)
|
||||
importing_frame = outer_frames[-1].frame
|
||||
try:
|
||||
source = inspect.getsource(importing_frame)
|
||||
except OSError:
|
||||
# stdin, compiled extensions, etc.
|
||||
return
|
||||
|
||||
node = ast.parse(source)
|
||||
names = parse_names(node)
|
||||
install_packages(names)
|
||||
frontmatter = generate_frontmatter(names, mode="str")
|
||||
exec(frontmatter, importing_frame.f_globals, importing_frame.f_locals)
|
||||
|
||||
|
||||
def install_packages(names: NameCollection):
|
||||
"""
|
||||
Try to install any packages we can't import!
|
||||
"""
|
||||
quiet = bool(os.environ.get('NMI_QUIET', False))
|
||||
to_install = []
|
||||
for name in names:
|
||||
base_module = name.module.split('.')[0]
|
||||
if base_module in sys.stdlib_module_names or base_module in sys.builtin_module_names:
|
||||
continue
|
||||
do_install = False
|
||||
try:
|
||||
spec = find_spec(base_module)
|
||||
if spec is None:
|
||||
do_install = True
|
||||
except ModuleNotFoundError:
|
||||
do_install = True
|
||||
|
||||
if do_install:
|
||||
to_install.append(base_module)
|
||||
|
||||
if not to_install:
|
||||
return
|
||||
|
||||
_do_install(to_install, quiet)
|
||||
|
||||
|
||||
def _do_install(packages: list[str], quiet: bool = False):
|
||||
if len(packages) == 0:
|
||||
return
|
||||
if not quiet:
|
||||
print(f"we're gonna try to install some stuff:\n {packages}")
|
||||
|
||||
errors = []
|
||||
for package in packages:
|
||||
res = subprocess.run(['python', '-m', 'pip', 'install', package], capture_output=True)
|
||||
if res.returncode != 0:
|
||||
errors.append({'package': package, 'stdout': res.stdout, 'stderr': res.stderr})
|
||||
if len(errors) == 0:
|
||||
print("sweet jesus we did it")
|
||||
else:
|
||||
print(f"some problems here pal:\n{pformat(errors, indent=2, compact=True)}")
|
||||
|
||||
|
||||
def install():
|
||||
patch_importing_frame()
|
||||
sys.meta_path.insert(0, LazyFinder())
|
|
@ -1,6 +1,7 @@
|
|||
import lazy_import
|
||||
from pathlib import Path
|
||||
|
||||
__all__ = ['lazy_import']
|
||||
import no_more_imports
|
||||
|
||||
DATA_DIR = Path(__file__).resolve().parent / 'data'
|
||||
__all__ = ["no_more_imports"]
|
||||
|
||||
DATA_DIR = Path(__file__).resolve().parent / "data"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import lazy_import
|
||||
import no_more_imports
|
||||
import collections.abc
|
||||
import json as jay_son
|
||||
from typing import List
|
||||
|
@ -7,8 +7,8 @@ from collections import ChainMap as cm
|
|||
|
||||
mod_variable = 10
|
||||
|
||||
my_list = [1,2,3]
|
||||
my_dict = {'a': 1, 'b':2}
|
||||
my_list = [1, 2, 3]
|
||||
my_dict = {"a": 1, "b": 2}
|
||||
list_comprehension = [item for item in my_list]
|
||||
dict_comprehension = {key: val for key, val in my_dict.items()}
|
||||
|
||||
|
@ -19,24 +19,25 @@ for item in my_list:
|
|||
def imports_are_lazy():
|
||||
ast
|
||||
typing.List
|
||||
re.match('hell ya', 'hell ya')
|
||||
match('hell ya again', 'hell ya again')
|
||||
re.match("hell ya", "hell ya")
|
||||
match("hell ya again", "hell ya again")
|
||||
# one day you should be able to do this
|
||||
# numpy as np
|
||||
|
||||
def a_function(a: typing.Iterable) -> importlib.abc.Finder:
|
||||
a = 1
|
||||
|
||||
def another_function(a: 'pathlib.Path') -> 'os.path.basename':
|
||||
def another_function(a: "pathlib.Path") -> "os.path.basename":
|
||||
pass
|
||||
|
||||
class AClass():
|
||||
class AClass:
|
||||
zz = 1
|
||||
yy = array.array()
|
||||
|
||||
def __init__(self):
|
||||
_ = base64.b64decode(f"{binascii.hexlify(b'abc')}")
|
||||
|
||||
|
||||
def regular_names_still_work():
|
||||
# assert mod_variable == 10
|
||||
x = 20
|
||||
|
@ -47,12 +48,12 @@ def regular_names_still_work():
|
|||
assert z == 10
|
||||
assert y == 30
|
||||
|
||||
class AClass():
|
||||
class AClass:
|
||||
z = 1
|
||||
|
||||
|
||||
afunc(30)
|
||||
|
||||
|
||||
def test_names_are_lazy():
|
||||
"""
|
||||
you can just use the last unique segment
|
||||
|
@ -60,5 +61,9 @@ def test_names_are_lazy():
|
|||
_ = random.randint(1, 10)
|
||||
_ = randint(1, 10)
|
||||
|
||||
a = numpy.random.default_rng()
|
||||
ints = a.integers((1,2))
|
||||
|
||||
assert randint is random.randint
|
||||
|
||||
|
||||
|
|
|
@ -1,43 +1,30 @@
|
|||
import ast
|
||||
|
||||
from lazy_import.ast import flatten_attribute, NameVisitor, NameCollection, Name
|
||||
from no_more_imports.ast import Name, NameCollection, NameVisitor
|
||||
|
||||
from .conftest import DATA_DIR
|
||||
|
||||
# def test_flatten_attribute():
|
||||
# attr = ast.Attribute(
|
||||
# value=ast.Attribute(
|
||||
# value=ast.Name(id='numpy'),
|
||||
# attr='random'),
|
||||
# attr='random'
|
||||
# )
|
||||
# assert isinstance(attr, ast.Attribute)
|
||||
#
|
||||
# assert flatten_attribute(attr) == "numpy.random.random"
|
||||
|
||||
def test_find_fake_names():
|
||||
expected = NameCollection(names=[Name(module='ast', name=None, aliases=set()),
|
||||
Name(module='typing', name='List', aliases=set()),
|
||||
Name(module='re', name='match', aliases={'match'}),
|
||||
Name(module='importlib.abc',
|
||||
name='Finder',
|
||||
aliases=set()),
|
||||
expected = NameCollection(
|
||||
names=[
|
||||
Name(module="ast", name=None, aliases=set()),
|
||||
Name(module="typing", name="List", aliases=set()),
|
||||
Name(module="re", name="match", aliases={"match"}),
|
||||
Name(module="importlib.abc", name="Finder", aliases=set()),
|
||||
Name(module="os.path", name="basename", aliases=set()),
|
||||
Name(module="pathlib", name="Path", aliases=set()),
|
||||
Name(module="array", name="array", aliases=set()),
|
||||
Name(module="base64", name="b64decode", aliases=set()),
|
||||
Name(module="binascii", name="hexlify", aliases=set()),
|
||||
Name(module="random", name="randint", aliases={"randint"}),
|
||||
]
|
||||
)
|
||||
|
||||
Name(module='os.path', name='basename', aliases=set()),
|
||||
Name(module='pathlib', name='Path', aliases=set()),
|
||||
Name(module='array', name='array', aliases=set()),
|
||||
Name(module='base64', name='b64decode', aliases=set()),
|
||||
Name(module='binascii', name='hexlify', aliases=set()),
|
||||
Name(module='random',
|
||||
name='randint',
|
||||
aliases={'randint'}),
|
||||
])
|
||||
|
||||
with open(DATA_DIR / 'input_file.py', 'r') as sfile:
|
||||
with open(DATA_DIR / "input_file.py", "r") as sfile:
|
||||
source_code = sfile.read()
|
||||
|
||||
node = ast.parse(source_code)
|
||||
visitor = NameVisitor()
|
||||
visitor.visit(node)
|
||||
assert visitor.fake_names == expected
|
||||
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import lazy_import
|
||||
from typing import List
|
||||
import collections.abc
|
||||
from collections.abc import Callable
|
||||
from collections import ChainMap as cm
|
||||
import no_more_imports
|
||||
|
||||
|
||||
mod_variable = 10
|
||||
|
||||
|
||||
def test_imports_are_lazy():
|
||||
re.match('hell ya', 'hell ya')
|
||||
re.match("hell ya", "hell ya")
|
||||
typing.List
|
||||
|
||||
|
||||
def test_regular_names_still_work():
|
||||
assert mod_variable == 10
|
||||
x = 20
|
||||
|
@ -22,6 +21,7 @@ def test_regular_names_still_work():
|
|||
|
||||
afunc(30)
|
||||
|
||||
|
||||
def test_names_are_lazy():
|
||||
"""
|
||||
you can just use the last unique segment
|
||||
|
@ -30,3 +30,14 @@ def test_names_are_lazy():
|
|||
_ = randint(1, 10)
|
||||
|
||||
assert randint is random.randint
|
||||
|
||||
|
||||
def test_even_installs_are_lazy():
|
||||
"""
|
||||
whatever, if we don't even have the package we'll try to get it
|
||||
"""
|
||||
res = subprocess.run(['python', '-m', 'pip', 'uninstall', 'numpy', '-y'])
|
||||
assert res.returncode == 0
|
||||
|
||||
data = numpy.zeros((2,2))
|
||||
assert numpy.array_equal(data, numpy.zeros((2,2)))
|
||||
|
|
Loading…
Reference in a new issue