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:
sneakers-the-rat 2024-10-16 23:17:57 -07:00
parent b0996c43eb
commit 453c0dcece
Signed by untrusted user who does not match committer: jonny
GPG key ID: 6DCB96EF1E4D232D
13 changed files with 544 additions and 273 deletions

106
README.md
View file

@ -1,5 +1,107 @@
# lazy-import # no-more-imports
python with no 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
View file

@ -2,21 +2,72 @@
# It is not intended for manual editing. # It is not intended for manual editing.
[metadata] [metadata]
groups = ["default", "tests"] groups = ["default", "dev", "tests"]
strategy = ["inherit_metadata"] strategy = ["inherit_metadata"]
lock_version = "4.5.0" lock_version = "4.5.0"
content_hash = "sha256:7fbe8fcf82251321bd2b6ad10d735dd1e657cbf01fed87457fcd3e33a453592a" content_hash = "sha256:48f2f19e324f95afaeb97d64e1d9bc9b95df7986ac7897935eca473952d81844"
[[metadata.targets]] [[metadata.targets]]
requires_python = ">=3.10" 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]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
summary = "Cross-platform colored terminal text." summary = "Cross-platform colored terminal text."
groups = ["tests"] groups = ["dev", "tests"]
marker = "sys_platform == \"win32\"" marker = "sys_platform == \"win32\" or platform_system == \"Windows\""
files = [ files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
@ -46,65 +97,14 @@ files = [
] ]
[[package]] [[package]]
name = "numpy" name = "mypy-extensions"
version = "2.1.2" version = "1.0.0"
requires_python = ">=3.10" requires_python = ">=3.5"
summary = "Fundamental package for array computing in Python" summary = "Type system extensions for programs checked with the mypy type checker."
groups = ["tests"] groups = ["dev"]
files = [ files = [
{file = "numpy-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30d53720b726ec36a7f88dc873f0eec8447fbc93d93a8f079dfac2629598d6ee"}, {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "numpy-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8d3ca0a72dd8846eb6f7dfe8f19088060fcb76931ed592d29128e0219652884"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
{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"},
] ]
[[package]] [[package]]
@ -112,12 +112,34 @@ name = "packaging"
version = "24.1" version = "24.1"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "Core utilities for Python packages" summary = "Core utilities for Python packages"
groups = ["tests"] groups = ["dev", "tests"]
files = [ files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, {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]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.5.0" version = "1.5.0"
@ -148,14 +170,53 @@ files = [
{file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, {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]] [[package]]
name = "tomli" name = "tomli"
version = "2.0.2" version = "2.0.2"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "A lil' TOML parser" summary = "A lil' TOML parser"
groups = ["tests"] groups = ["dev", "tests"]
marker = "python_version < \"3.11\"" marker = "python_version < \"3.11\""
files = [ files = [
{file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"},
{file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, {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"},
]

View file

@ -1,5 +1,5 @@
[project] [project]
name = "lazy-import" name = "no-more-imports"
version = "0.1.0" version = "0.1.0"
description = "python with no imports" description = "python with no imports"
authors = [ authors = [
@ -10,11 +10,40 @@ requires-python = ">=3.10"
readme = "README.md" readme = "README.md"
license = {text = "EUPL-1.2"} 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] [project.optional-dependencies]
tests = [ tests = [
"pytest>=8.3.3", "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] [build-system]
requires = ["pdm-backend"] requires = ["pdm-backend"]
build-backend = "pdm.backend" build-backend = "pdm.backend"

View file

@ -1,2 +0,0 @@
from lazy_import.importer import install
install()

View file

@ -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())

View file

@ -0,0 +1,3 @@
from no_more_imports.importer import install
install()

View file

@ -1,10 +1,9 @@
from typing import Literal, overload
import ast import ast
import pdb
from collections.abc import Container
from collections import ChainMap from collections import ChainMap
from collections.abc import Container
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from typing import Iterator, Literal, overload
@dataclass(eq=True) @dataclass(eq=True)
class Name: class Name:
@ -15,9 +14,7 @@ class Name:
def __contains__(self, item: str): def __contains__(self, item: str):
return item in self.aliases or ( return item in self.aliases or (
item == self.module item == self.module if self.name is None else item in (self.name, self.id)
if self.name is None else
item in (self.name, self.id)
) )
@property @property
@ -25,7 +22,7 @@ class Name:
if self.name is None: if self.name is None:
return self.module return self.module
else: else:
return '.'.join([self.module, self.name]) return ".".join([self.module, self.name])
@property @property
def parts(self) -> list[str]: def parts(self) -> list[str]:
@ -41,25 +38,26 @@ class Name:
- `module.submodule.subsubmodule.A` - `module.submodule.subsubmodule.A`
""" """
subparts = self.module.split('.') subparts = self.module.split(".")
if self.name: if self.name:
subparts.append(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): def in_dict(self, other: Container):
return any(part in other for part in self.parts) return any(part in other for part in self.parts)
@classmethod @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) return cls.from_str(name.id)
@classmethod @classmethod
def from_str(cls, name: str) -> 'Name': def from_str(cls, name: str) -> "Name":
if len(name_parts := name.rsplit(".", maxsplit=1)) > 1: if len(name_parts := name.rsplit(".", maxsplit=1)) > 1:
return Name(module=name_parts[0], name=name_parts[1]) return Name(module=name_parts[0], name=name_parts[1])
else: else:
return Name(module=name) return Name(module=name)
@dataclass(eq=True) @dataclass(eq=True)
class NameCollection: class NameCollection:
@ -81,6 +79,9 @@ class NameCollection:
self.names.append(new_name) self.names.append(new_name)
def __iter__(self) -> Iterator[Name]:
yield from self.names
class NameVisitor(ast.NodeVisitor): class NameVisitor(ast.NodeVisitor):
""" """
@ -88,7 +89,7 @@ class NameVisitor(ast.NodeVisitor):
""" """
def __init__(self): def __init__(self):
self.real_names = ChainMap({'self': None}, globals()['__builtins__']) self.real_names = ChainMap({"self": None}, globals()["__builtins__"])
self.fake_names = NameCollection() self.fake_names = NameCollection()
def pop_ctx(self): def pop_ctx(self):
@ -109,8 +110,6 @@ class NameVisitor(ast.NodeVisitor):
"""Add to names""" """Add to names"""
return self.visit_Import(node) return self.visit_Import(node)
def visit_Name(self, node: ast.Name): def visit_Name(self, node: ast.Name):
"""Either add to real names or fake names depending on ctx""" """Either add to real names or fake names depending on ctx"""
# print(ast.dump(node)) # print(ast.dump(node))
@ -124,7 +123,7 @@ class NameVisitor(ast.NodeVisitor):
del self.real_names[node.id] del self.real_names[node.id]
else: # pragma: no cover else: # pragma: no cover
if type(node.ctx) not in (ast.Del, ast.Store, ast.Load): 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): def visit_Attribute(self, node: ast.Attribute):
# print(ast.dump(node)) # print(ast.dump(node))
@ -142,17 +141,17 @@ class NameVisitor(ast.NodeVisitor):
del self.real_names[attr_name] del self.real_names[attr_name]
else: # pragma: no cover else: # pragma: no cover
if type(node.ctx) not in (ast.Del, ast.Store, ast.Load): 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): def visit_FunctionDef(self, node: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda):
"""push context""" """push context"""
# print(ast.dump(node)) # print(ast.dump(node))
# names that should be defined in the parent scope # 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) self._handle_annotation(node.returns)
if hasattr(node, 'name'): if hasattr(node, "name"):
self.real_names[node.name] = node self.real_names[node.name] = node
# enter function scope # enter function scope
@ -162,9 +161,9 @@ class NameVisitor(ast.NodeVisitor):
self.real_names[arg.arg] = arg self.real_names[arg.arg] = arg
if arg.annotation: if arg.annotation:
self._handle_annotation(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 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.real_names[args.kwarg.arg] = args.kwarg
self.generic_visit(node) self.generic_visit(node)
@ -213,10 +212,10 @@ class NameVisitor(ast.NodeVisitor):
self.real_names[gen.target.id] = gen.target self.real_names[gen.target.id] = gen.target
self.generic_visit(node) self.generic_visit(node)
self.pop_ctx() self.pop_ctx()
# if isinstance(gen.iter, ast.Name): # if isinstance(gen.iter, ast.Name):
# self.visit_Name(gen.iter) # self.visit_Name(gen.iter)
# elif isinstance(gen.iter, ast.Call): # elif isinstance(gen.iter, ast.Call):
# self.visit_Attribute(gen.iter.func) # self.visit_Attribute(gen.iter.func)
def visit_DictComp(self, node: ast.DictComp): def visit_DictComp(self, node: ast.DictComp):
self.visit_ListComp(node) self.visit_ListComp(node)
@ -250,56 +249,76 @@ class NameVisitor(ast.NodeVisitor):
After visiting, we remove top-level module level definitions from 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 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
]
def parse_names(node: ast.AST) -> NameCollection:
@overload """
def generate_frontmatter(node: ast.AST, mode: Literal['ast'] = 'ast') -> list[ast.Import | ast.Assign]: ... Get the names that need to be imported from an AST module by applying
:class:`.NameVisitor`
@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:
visitor = NameVisitor() visitor = NameVisitor()
visitor.visit(node) 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) @overload
elif mode == 'str': def generate_frontmatter(
return _frontmatter_str(modules, visitor) 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: else:
raise ValueError("Unknown frontmatter mode") 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] imports = [ast.Import(names=[ast.alias(name)]) for name in modules]
assignments = [] assignments = []
for name in visitor.fake_names.names: for name in names.names:
for alias in name.aliases: for alias in name.aliases:
assignments.append( assignments.append(
ast.Assign(targets=[ast.Name(id=alias, ctx=ast.Store())], ast.Assign(
value=ast.Name(id=name.id, ctx=ast.Load())) targets=[ast.Name(id=alias, ctx=ast.Store())],
value=ast.Name(id=name.id, ctx=ast.Load()),
)
) )
return imports + assignments 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 = [] assignments = []
for name in visitor.fake_names.names: for name in names.names:
for alias in name.aliases: for alias in name.aliases:
assignments.append(f'{alias} = {name.id}') assignments.append(f"{alias} = {name.id}")
return '\n'.join(imports + assignments)
return "\n".join(imports + assignments)
def flatten_attribute(attr: ast.Attribute) -> str: def flatten_attribute(attr: ast.Attribute) -> str:
if isinstance(attr.value, ast.Attribute): 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): 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

View file

@ -0,0 +1,6 @@
HARDCODED_SKIPS = [
"numpy"
]
"""
Stuff that we don't try and lazify because it's known to be broken
"""

View 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())

View file

@ -1,6 +1,7 @@
import lazy_import
from pathlib import Path 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"

View file

@ -1,4 +1,4 @@
import lazy_import import no_more_imports
import collections.abc import collections.abc
import json as jay_son import json as jay_son
from typing import List from typing import List
@ -7,8 +7,8 @@ from collections import ChainMap as cm
mod_variable = 10 mod_variable = 10
my_list = [1,2,3] my_list = [1, 2, 3]
my_dict = {'a': 1, 'b':2} my_dict = {"a": 1, "b": 2}
list_comprehension = [item for item in my_list] list_comprehension = [item for item in my_list]
dict_comprehension = {key: val for key, val in my_dict.items()} dict_comprehension = {key: val for key, val in my_dict.items()}
@ -19,24 +19,25 @@ for item in my_list:
def imports_are_lazy(): def imports_are_lazy():
ast ast
typing.List typing.List
re.match('hell ya', 'hell ya') re.match("hell ya", "hell ya")
match('hell ya again', 'hell ya again') match("hell ya again", "hell ya again")
# one day you should be able to do this # one day you should be able to do this
# numpy as np # numpy as np
def a_function(a: typing.Iterable) -> importlib.abc.Finder: def a_function(a: typing.Iterable) -> importlib.abc.Finder:
a = 1 a = 1
def another_function(a: 'pathlib.Path') -> 'os.path.basename': def another_function(a: "pathlib.Path") -> "os.path.basename":
pass pass
class AClass(): class AClass:
zz = 1 zz = 1
yy = array.array() yy = array.array()
def __init__(self): def __init__(self):
_ = base64.b64decode(f"{binascii.hexlify(b'abc')}") _ = base64.b64decode(f"{binascii.hexlify(b'abc')}")
def regular_names_still_work(): def regular_names_still_work():
# assert mod_variable == 10 # assert mod_variable == 10
x = 20 x = 20
@ -47,12 +48,12 @@ def regular_names_still_work():
assert z == 10 assert z == 10
assert y == 30 assert y == 30
class AClass(): class AClass:
z = 1 z = 1
afunc(30) afunc(30)
def test_names_are_lazy(): def test_names_are_lazy():
""" """
you can just use the last unique segment you can just use the last unique segment
@ -60,5 +61,9 @@ def test_names_are_lazy():
_ = random.randint(1, 10) _ = random.randint(1, 10)
_ = randint(1, 10) _ = randint(1, 10)
a = numpy.random.default_rng()
ints = a.integers((1,2))
assert randint is random.randint assert randint is random.randint

View file

@ -1,43 +1,30 @@
import ast 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 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(): def test_find_fake_names():
expected = NameCollection(names=[Name(module='ast', name=None, aliases=set()), expected = NameCollection(
Name(module='typing', name='List', aliases=set()), names=[
Name(module='re', name='match', aliases={'match'}), Name(module="ast", name=None, aliases=set()),
Name(module='importlib.abc', Name(module="typing", name="List", aliases=set()),
name='Finder', Name(module="re", name="match", aliases={"match"}),
aliases=set()), 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()), with open(DATA_DIR / "input_file.py", "r") as sfile:
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:
source_code = sfile.read() source_code = sfile.read()
node = ast.parse(source_code) node = ast.parse(source_code)
visitor = NameVisitor() visitor = NameVisitor()
visitor.visit(node) visitor.visit(node)
assert visitor.fake_names == expected assert visitor.fake_names == expected

View file

@ -1,15 +1,14 @@
import lazy_import import no_more_imports
from typing import List
import collections.abc
from collections.abc import Callable
from collections import ChainMap as cm
mod_variable = 10 mod_variable = 10
def test_imports_are_lazy(): def test_imports_are_lazy():
re.match('hell ya', 'hell ya') re.match("hell ya", "hell ya")
typing.List typing.List
def test_regular_names_still_work(): def test_regular_names_still_work():
assert mod_variable == 10 assert mod_variable == 10
x = 20 x = 20
@ -22,6 +21,7 @@ def test_regular_names_still_work():
afunc(30) afunc(30)
def test_names_are_lazy(): def test_names_are_lazy():
""" """
you can just use the last unique segment you can just use the last unique segment
@ -30,3 +30,14 @@ def test_names_are_lazy():
_ = randint(1, 10) _ = randint(1, 10)
assert randint is random.randint 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)))