├── .coveragerc
├── .github
└── workflows
│ └── blank.yml
├── .gitignore
├── .idea
├── .gitignore
├── inspectionProfiles
│ └── profiles_settings.xml
├── justuse.iml
├── misc.xml
├── modules.xml
└── vcs.xml
├── .pyup.yml
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.MD
├── LICENSE
├── MANIFEST.in
├── README.md
├── codecov.yml
├── docs
├── Showcase.ipynb
├── codeflow.md
├── database schema.md
├── demo.py
├── module_a.py
├── module_b.py
├── module_circular_a.py
└── module_circular_b.py
├── extra
├── logo.png
├── logo.svg
├── logo1.svg
└── web-message-mockup.pptx
├── logo.png
├── pytest.ini
├── requirements.txt
├── setup.py
├── src
├── .test4.py
├── .test5.py
├── .test6.py
├── .test7.py
├── .test8.py
├── .test9.py
├── pytest.ini
├── testfile.whl
└── use
│ ├── __init__.py
│ ├── aspectizing.py
│ ├── buffet.py
│ ├── buffet_old.py
│ ├── hash_alphabet.py
│ ├── logutil.py
│ ├── main.py
│ ├── messages.py
│ ├── pimp.py
│ ├── pydantics.py
│ ├── templates
│ ├── aspects.css
│ ├── aspects.html
│ ├── aspects_dry_run.html
│ ├── hash-presentation.html
│ ├── profiling.css
│ ├── profiling.html
│ └── stylesheet.css
│ ├── test.py
│ └── tools.py
└── tests
├── .tests
├── .file_for_test387.py
├── .test0.py
├── .test1.py
├── .test2.py
├── .test3.py
├── bar.py
├── discord_enum.py
├── foo.py
├── modA.py
├── modA_test.py
├── modB.py
├── modD.py
├── modE.py
├── sys.py
├── tests_subdir
│ └── modC.py
└── trained_tagger.pkl
├── __init__.py
├── beast.py
├── beast_data.json
├── beast_data_preparation.py
├── coverage.sh
├── coverage_badge.sh
├── coverage_combine.sh
├── foo.py
├── integration
├── README.md
├── __init__.py
├── collect_packages.py
├── docker-compose.yaml
├── dockerfile
├── integration_test.py
├── justtest.py
├── pypi.json
├── requirements.txt
├── test_pypi_model.py
├── test_single.py
├── tmp.json
└── tmp.py
├── mass_test_initdata.json
├── simple_funcs.py
├── tdd_test.py
├── test.py
└── unit_test.py
/.coveragerc:
--------------------------------------------------------------------------------
1 |
2 | [report]
3 | ignore_errors = True
4 |
5 | [run]
6 | omit =
7 | tests/*
8 | tests/.*
9 | tests/mass/*
10 | *_test
11 | .*
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.github/workflows/blank.yml:
--------------------------------------------------------------------------------
1 |
2 | name: Unit Tests
3 |
4 |
5 | on:
6 | fork:
7 | pull_request:
8 | types: [opened, edited, closed]
9 | push:
10 | release:
11 | types: [published, created, edited, released]
12 |
13 | jobs:
14 | test-ubuntu:
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - name: Linux Tests
19 | uses: actions/checkout@v2
20 | - name: Ubuntu - Set up Python
21 | uses: actions/setup-python@v2
22 | with:
23 | python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax
24 | architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
25 | - run: |
26 | echo "Linux-Tests: Preloading package dependencies ..."
27 | python3 -m pip --disable-pip-version-check --no-python-version-warning install anyio mmh3 mypy packaging pip pytest pytest-env pytest-cov requests types-toml types-requests furl
28 | echo "Linux-Tests: Running tests ..."
29 | mkdir -p ~/.justuse-python/
30 | echo "debug = true" > ~/.justuse-python/config.toml
31 | python3 -m pip --disable-pip-version-check --no-python-version-warning install --force-reinstall --upgrade -r requirements.txt
32 | python3 -m pip --disable-pip-version-check --no-python-version-warning install -r requirements.txt furl
33 | IFS=$'\n'; set -- $( find -name "*.py" | cut -c 3- | sed -r -e 's~^src/~~; s~\.py~~; \~^\.|/\.~d; s~/~.~g; s~\.__init__$~~; s~^~--cov=~; ' ; );
34 | python3 -m pytest --cov-branch --cov-report term-missing --cov-report html:coverage/ --cov-report annotate:coverage/annotated --cov-report xml:coverage/cov.xml "$@" tests/unit_test.py
35 | - name: Upload Coverage to Codecov
36 | uses: codecov/codecov-action@v2
37 | with:
38 | token: 20fb71ba-3e2b-46db-b86d-e1666d56665b
39 | fail_ci_if_error: false
40 | name: justuse
41 | files: .coverage,coverage/cov.xml
42 | verbose: true
43 |
44 | test-macos:
45 | runs-on: macos-11
46 |
47 | steps:
48 | - name: Linux Tests
49 | uses: actions/checkout@v2
50 | - name: Ubuntu - Set up Python
51 | uses: actions/setup-python@v2
52 | with:
53 | python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax
54 | architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
55 | - run: |
56 | echo "Linux-Tests: Preloading package dependencies ..."
57 | python3 -m pip --disable-pip-version-check --no-python-version-warning install anyio mmh3 mypy packaging pip pytest pytest-env pytest-cov requests types-toml types-requests furl
58 | echo "Linux-Tests: Running tests ..."
59 | mkdir -p ~/.justuse-python/
60 | echo "debug = true" > ~/.justuse-python/config.toml
61 | python3 -m pip --disable-pip-version-check --no-python-version-warning install --force-reinstall --upgrade -r requirements.txt
62 | python3 -m pip --disable-pip-version-check --no-python-version-warning install -r requirements.txt furl
63 | IFS=$'\n'; set -- $( find . -name "*.py" | cut -c 3- | sed -r -e 's~^src/~~; s~\.py~~; \~^\.|/\.~d; s~/~.~g; s~\.__init__$~~; s~^~--cov=~; ' ; );
64 | python3 -m pytest --cov-branch --cov-report term-missing --cov-report html:coverage/ --cov-report annotate:coverage/annotated --cov-report xml:coverage/cov.xml "$@" tests/unit_test.py
65 | - name: Upload Coverage to Codecov
66 | uses: codecov/codecov-action@v2
67 | with:
68 | token: 20fb71ba-3e2b-46db-b86d-e1666d56665b
69 | fail_ci_if_error: false
70 | name: justuse
71 | files: .coverage,coverage/cov.xml
72 | verbose: true
73 |
74 | build-dist:
75 | runs-on: ubuntu-latest
76 | steps:
77 | - name: Check out source
78 | uses: actions/checkout@v2
79 | - name: Ubuntu - Set up Python
80 | uses: actions/setup-python@v2
81 | with:
82 | python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax
83 | architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
84 | - name: Build wheel and sdist
85 | run: |
86 | python3 -m pip --disable-pip-version-check --no-python-version-warning install setuptools wheel 2>/dev/null 1>&2 || true
87 | python3 setup.py egg_info
88 | python3 setup.py bdist_wheel
89 | python3 setup.py sdist
90 | SUFFIX="$( python3 -c $'def timestamp2fragday():\n from datetime import datetime; from math import modf\n now = datetime.utcnow()\n seconds = now.hour*60*60 + now.minute*60 + now.second\n total_seconds = 24*60*60\n return datetime.today().strftime("%Y%m%d") + f".{modf(seconds/total_seconds)[0]:.4f}"[1:].strip("0")\nprint(timestamp2fragday())'; )"
91 | typeset -p SUFFIX
92 | echo $'\n'"ARTIFACT_BDIST_PATH=$( find dist -name '*.whl' -printf '%p'; )"$'\n' | tee -a "$GITHUB_ENV"
93 | echo $'\n'"ARTIFACT_SDIST_PATH=$( find dist -name '*.tar*' -printf '%p'; )"$'\n' | tee -a "$GITHUB_ENV"
94 | echo $'\n'"ARTIFACT_BDIST_NAME=justuse-$SUFFIX"$'\n' | tee -a "$GITHUB_ENV"
95 | echo $'\n'"ARTIFACT_SDIST_NAME=justuse-$SUFFIX"$'\n' | tee -a "$GITHUB_ENV"
96 | - name: Upload Artifact - bdist
97 | uses: actions/upload-artifact@v2
98 | with:
99 | name: "${{ env.ARTIFACT_BDIST_NAME }}"
100 | path: "${{ env.ARTIFACT_BDIST_PATH }}"
101 | - name: Upload Artifact - sdist
102 | uses: actions/upload-artifact@v2
103 | with:
104 | name: "${{ env.ARTIFACT_SDIST_NAME }}"
105 | path: "${{ env.ARTIFACT_SDIST_PATH }}"
106 |
107 |
108 | test-windows-x86:
109 | runs-on: windows-latest
110 | steps:
111 | - name: Windows - Check out source
112 | uses: actions/checkout@v2
113 | - name: Windows - Set up Python
114 | uses: actions/setup-python@v2
115 | with:
116 | python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax
117 | architecture: 'x86' # optional x64 or x86. Defaults to x64 if not specified
118 | - name: Windows - Run Unit Tests with Coverage
119 | run: |
120 | $env:FTP_USER = "${{ secrets.FTP_USER }}"
121 | $env:FTP_PASS = "${{ secrets.FTP_PASS }}"
122 | $env:DEBUG = 1
123 | $env:DEBUGGING = 1
124 | $env:ERRORS = 1
125 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
126 | & "python.exe" -m pip --disable-pip-version-check --no-python-version-warning install --exists-action s --prefer-binary --no-compile --upgrade -r requirements.txt
127 | & "python.exe" -m pytest --cov-branch --cov-report term-missing --cov-report html:coverage-win32-x86/ --cov-report annotate:coverage-win32-x86/annotated --cov-report xml:coverage-win32-x86/cov.xml --cov=setup --cov=use --cov=use.hash_alphabet --cov=use.main --cov=use.messages --cov=use.mod --cov=use.pimp --cov=use.platformtag --cov=use.pypi_model --cov=use.tools --cov=tests --cov=tests.foo --cov=tests.mass.collect_packages --cov=tests.mass.justtest --cov=tests.mass.test_pypi_model --cov=tests.mass.test_single --cov=tests.mass.tmp --cov=tests.simple_funcs --cov=tests.tdd_test --cov=tests.test --cov=tests.test_beast --cov=tests.unit_test --cov=build.lib.use --cov=build.lib.use.hash_alphabet --cov=build.lib.use.main --cov=build.lib.use.messages --cov=build.lib.use.mod --cov=build.lib.use.pimp --cov=build.lib.use.platformtag --cov=build.lib.use.pypi_model --cov=build.lib.use.tools tests/unit_test.py
128 | - name: Windows - Collect Coverage
129 | run: |
130 | & "xcopy.exe" ".\.coverage" ".\coverage-win32-x86"
131 | - name: Collect Coverage
132 | uses: master-atul/tar-action@v1.0.2
133 | id: compress
134 | with:
135 | command: c
136 | cwd: .
137 | files: |
138 | ./.coverage
139 | ./coverage-win32-x86
140 | outPath: coverage-win32-x86.tar.gz
141 | - name: Upload Artifact
142 | uses: actions/upload-artifact@v2
143 | with:
144 | name: coverage
145 | path: coverage-win32-x86.tar.gz
146 |
147 |
148 |
149 | test-windows-x64:
150 | runs-on: windows-latest
151 | steps:
152 | - name: Windows - Check out source
153 | uses: actions/checkout@v2
154 | - name: Windows - Set up Python
155 | uses: actions/setup-python@v2
156 | with:
157 | python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax
158 | architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
159 | - name: Windows - Run Unit Tests with Coverage
160 | run: |
161 | $env:FTP_USER = "${{ secrets.FTP_USER }}"
162 | $env:FTP_PASS = "${{ secrets.FTP_PASS }}"
163 | $env:DEBUG = 1
164 | $env:DEBUGGING = 1
165 | $env:ERRORS = 1
166 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
167 | & "python.exe" -m pip --disable-pip-version-check --no-python-version-warning install --exists-action s --prefer-binary --no-compile --upgrade -r requirements.txt
168 | & "python.exe" -m pytest --cov-branch --cov-report term-missing --cov-report html:coverage-win32-x64/ --cov-report annotate:coverage-win32-x64/annotated --cov-report xml:coverage-win32-x64/cov.xml --cov=setup --cov=use --cov=use.hash_alphabet --cov=use.main --cov=use.messages --cov=use.mod --cov=use.pimp --cov=use.platformtag --cov=use.pypi_model --cov=use.tools --cov=tests --cov=tests.foo --cov=tests.mass.collect_packages --cov=tests.mass.justtest --cov=tests.mass.test_pypi_model --cov=tests.mass.test_single --cov=tests.mass.tmp --cov=tests.simple_funcs --cov=tests.tdd_test --cov=tests.test --cov=tests.test_beast --cov=tests.unit_test --cov=build.lib.use --cov=build.lib.use.hash_alphabet --cov=build.lib.use.main --cov=build.lib.use.messages --cov=build.lib.use.mod --cov=build.lib.use.pimp --cov=build.lib.use.platformtag --cov=build.lib.use.pypi_model --cov=build.lib.use.tools tests/unit_test.py
169 | - name: Windows - Collect Coverage
170 | run: |
171 | & "xcopy.exe" ".\.coverage" ".\coverage-win32-x64"
172 | - name: Collect Coverage
173 | uses: master-atul/tar-action@v1.0.2
174 | id: compress
175 | with:
176 | command: c
177 | cwd: .
178 | files: |
179 | ./.coverage
180 | ./coverage-win32-x64
181 | outPath: coverage-win32-x64.tar.gz
182 | - name: Upload Artifact
183 | uses: actions/upload-artifact@v2
184 | with:
185 | name: coverage
186 | path: coverage-win32-x64.tar.gz
187 |
188 |
189 |
190 | name: Create Release
191 |
192 | on:
193 | push:
194 | tags:
195 | - '*.*.*'
196 |
197 | jobs:
198 | build:
199 | name: Create release
200 | runs-on: ubuntu-latest
201 | steps:
202 | - name: Checkout
203 | uses: actions/checkout@v3
204 | - name: Extract release notes
205 | id: extract-release-notes
206 | uses: ffurrer2/extract-release-notes@v1
207 | - name: Create release
208 | uses: actions/create-release@v1
209 | env:
210 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
211 | with:
212 | tag_name: ${{ github.ref }}
213 | release_name: ${{ github.ref }}
214 | draft: false
215 | prerelease: false
216 | body: ${{ steps.extract-release-notes.outputs.release_notes }}
217 |
218 |
219 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 | workspace.code-workspace
131 | tests/.test3.py
132 | tests/.test2.py
133 | tests/.test1.py
134 | src/use/.test4.py
135 | Asylum.ipynb
136 | local_switch
137 | .vscode/launch.json
138 | .vscode/settings.json
139 | tests/.tests/.test3.py
140 | tests/.tests/asdf.py
141 |
142 | # misc backup
143 | *.1*.bak
144 | *.1*.mod
145 | *.orig
146 | *.rej
147 | .gdb_history
148 | coverage/
149 | /coverage
150 | coverage/**
151 | coverage/*
152 | coverage.json
153 | ?_*.sql
154 | *.db
155 | core.*
156 | release.py
157 | releases/**
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/justuse.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.pyup.yml:
--------------------------------------------------------------------------------
1 | # autogenerated pyup.io config file
2 | # see https://pyup.io/docs/configuration/ for all available options
3 |
4 | schedule: ''
5 | update: insecure
6 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "sourcery.sourcery"
4 | ]
5 | }
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 |
8 | {
9 | "name": "Python: Aktuelle Datei",
10 | "type": "debugpy",
11 | "request": "launch",
12 | "program": "${file}",
13 | "console": "integratedTerminal",
14 | "justMyCode": true,
15 | "env": {"PYTEST_ADDOPTS": "--no-cov"}
16 | }
17 | ]
18 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "python.testing.pytestArgs": [
4 | "."
5 | ],
6 | "python.testing.unittestEnabled": false,
7 | "python.testing.pytestEnabled": true,
8 | "python.formatting.provider": "black",
9 | "python.analysis.typeCheckingMode": "off"
10 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amogorkon/justuse/3a478fee76f728f681977ee8bf0738747317bc7f/CHANGELOG.md
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 |
2 | # Contributor Covenant Code of Conduct
3 |
4 | ## Our Pledge
5 |
6 | In the interest of fostering an open and welcoming environment, we as
7 | contributors and maintainers pledge to make participation in our project and
8 | our community a harassment-free experience for everyone, regardless of age, body
9 | size, disability, ethnicity, sex characteristics, gender identity and expression,
10 | level of experience, education, socio-economic status, nationality, personal
11 | appearance, race, religion, or sexual identity and orientation.
12 |
13 | ## Our Standards
14 |
15 | Examples of behavior that contributes to creating a positive environment
16 | include:
17 |
18 | * Using welcoming and inclusive language
19 | * Being respectful of differing viewpoints and experiences
20 | * Gracefully accepting constructive criticism
21 | * Focusing on what is best for the community
22 | * Showing empathy towards other community members
23 |
24 | Examples of unacceptable behavior by participants include:
25 |
26 | * The use of sexualized language or imagery and unwelcome sexual attention or
27 | advances
28 | * Trolling, insulting/derogatory comments, and personal or political attacks
29 | * Public or private harassment
30 | * Publishing others' private information, such as a physical or electronic
31 | address, without explicit permission
32 | * Other conduct which could reasonably be considered inappropriate in a
33 | professional setting
34 |
35 | ## Our Responsibilities
36 |
37 | Project maintainers are responsible for clarifying the standards of acceptable
38 | behavior and are expected to take appropriate and fair corrective action in
39 | response to any instances of unacceptable behavior.
40 |
41 | Project maintainers have the right and responsibility to remove, edit, or
42 | reject comments, commits, code, wiki edits, issues, and other contributions
43 | that are not aligned to this Code of Conduct, or to ban temporarily or
44 | permanently any contributor for other behaviors that they deem inappropriate,
45 | threatening, offensive, or harmful.
46 |
47 | ## Scope
48 |
49 | This Code of Conduct applies within all project spaces, and it also applies when
50 | an individual is representing the project or its community in public spaces.
51 | Examples of representing a project or community include using an official
52 | project e-mail address, posting via an official social media account, or acting
53 | as an appointed representative at an online or offline event. Representation of
54 | a project may be further defined and clarified by project maintainers.
55 |
56 | ## Enforcement
57 |
58 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
59 | reported by contacting the project team at [justuse-github@anselm.kiefner.de]. All
60 | complaints will be reviewed and investigated and will result in a response that
61 | is deemed necessary and appropriate to the circumstances. The project team is
62 | obligated to maintain confidentiality with regard to the reporter of an incident.
63 | Further details of specific enforcement policies may be posted separately.
64 |
65 | Project maintainers who do not follow or enforce the Code of Conduct in good
66 | faith may face temporary or permanent repercussions as determined by other
67 | members of the project's leadership.
68 |
69 | ## Attribution
70 |
71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
72 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
73 |
74 | [homepage]: https://www.contributor-covenant.org
75 |
76 | For answers to common questions about this code of conduct, see
77 | https://www.contributor-covenant.org/faq
78 |
--------------------------------------------------------------------------------
/CONTRIBUTING.MD:
--------------------------------------------------------------------------------
1 | Hi! Thank you for your interest in contributing to justuse :)
2 |
3 | Besides tackling [good first issues](https://github.com/amogorkon/justuse/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22), there is plenty other stuff to do - like writing unit tests and documentation, finding bugs and report them or coming up with new ideas and questions to keep us on our toes.
4 |
5 | For general ideas and questions, you can use [github discussions](https://github.com/amogorkon/justuse/discussions), for documentation please refer to our [wiki](https://github.com/amogorkon/justuse/wiki) and for discussions regarding code, and always feel free to join our [slack channel](https://app.slack.com/client/T0297SNMCHY).
6 |
7 | If you want to contribute code, be aware that we (at least I do) run black/brunette to enforce proper code formatting.
8 | You can use any indentation style (2,3,4 spaces, tabs..) you like on your side, but don't complain if they get mangled to 4-space indentation. Please follow PEP8 as far as is reasonable.
9 |
10 | Please make sure that your code is properly commented - always ask yourself "would future self immediately understand why I wrote it like this in a year from now?" if the answer is no, add a comment.
11 | And please try to refrain from code golfing. We all know the temptation, but.. :)
12 |
13 | To streamline commit messages, automatically managing issues and making them generally more useful for debugging, please follow [How to write good commit Messages](https://chiamakaikeanyi.dev/how-to-write-good-git-commit-messages/)
14 |
15 | Well, that's it. Welcome aboard!
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021-2023 Anselm Kiefner
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | graft src/use/templates/
2 | graft docs
3 |
4 | include README.md
5 | include LICENSE
6 | include CODE_OF_CONDUCT.md
7 | include requirements.txt
8 | recursive-include src
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/amogorkon/justuse/blob/main/LICENSE)
2 | [](https://github.com/amogorkon/justuse/stargazers)
3 | [](https://GitHub.com/amogorkon/justuse/graphs/commit-activity)
4 | [](https://pyup.io/repos/github/amogorkon/justuse/)
5 | [](https://github.com/amogorkon/justuse/actions/workflows/blank.yml)
6 | [](https://pepy.tech/project/justuse)
7 | [](https://snyk.io/advisor/python/justuse)
8 | [](https://join.slack.com/t/justuse/shared_invite/zt-tot4bhq9-_qIXBdeiRIfhoMjxu0EhFw)
9 | [](https://codecov.io/gh/amogorkon/justuse)
10 | [](https://sourcery.ai)
11 |
12 |
13 |
15 | # Just use() python the way you want!
16 |
17 |
18 | ```mermaid
19 | graph TD;
20 | A[just]-->B["use()"];
21 | B --> C["Path()"]
22 | B --> D["URL()"]
23 | B --> E[packages]
24 | B --> F[git]
25 | ```
26 |
27 | ## Installation
28 | To install, enter `python -m pip install justuse` in a commandline, then you can `import use` in your code and simply use() stuff. Check for examples below and our [Showcase](https://github.com/amogorkon/justuse/blob/unstable/docs/Showcase.ipynb)!
29 |
30 | ## Features, Claims & Goals
31 |
32 | - [x] inline version-checking
33 | - [x] safely import code from an online URL - towards an unhackable infrastructure ("Rather die than get corrupted!")
34 | - [x] initial module globals - a straight forward solution to diamond/cyclic imports
35 | - [x] decorate *everything* callable recursively via pattern matching, aspect-orientation made easy (except closures, those are *hard*)
36 | - [x] return a given default if an exception happened during an import, simplifying optional dependencies
37 | - [x] safe hot auto-reloading of function-only local modules - a REPL-like dev experience with files in jupyter and regular python interpreters
38 | - [x] signature compatibility checks for all callables in hot reloading modules as a first line of defense against invading bugs
39 | - [x] safely auto-install version-tagged pure python packages from PyPI
40 | - [x] have multiple versions of the same package installed and loaded in the same program without conflicts
41 | - [x] auto-install packages with C-extensions and other precompiled stuff
42 | - [x] no-hassle inline auto-installation of (almost) all conda packages
43 | - [ ] install packages directly from github with signature compatibility check for all callables
44 | - [ ] attach birdseye debugger to a loaded module as a mode
45 | - [ ] try to pull packages from a P2P network before pulling from PyPI or conda directly
46 | - [ ] all justuse-code is compiled to a single, standalone .py file - just drop it into your own code without installation
47 | - [ ] provide a visual representation of the internal dependency graph
48 | - [ ] module-level variable guards aka "module-properties"
49 | - [ ] isolation of software components with arbitrary sub-interpreters (python 2.7, ironpython..) inspired by [jupyter messaging](https://jupyter-client.readthedocs.io/en/latest/messaging.html)
50 | - [ ] slot-based plugin architecture (to ensure reproducable testability of plugin-combinations)
51 | - [ ] optional on-site compilation of fully annotated python code via cython
52 | - [ ] document everything!
53 | - [ ] test everything!
54 |
55 | ## The Story
56 | Over the years, many times I've come across various situations where Python's import statement just didn't work the way I needed.
57 | There were projects where I felt that a central module from where to expand functionality would be the simplest, most elegant approach, but that would only work with simple modules and libs, not with functionality that required access to the main state of the application. In those situations the first thing to try would be "import B" in module A and "import A" in module B - a classical circular import, which comes with a lot of headaches and often results in overly convoluted code. All this could be simplified if it was possible to pass some module-level global variables to the about-to-be-imported module before its actual execution, but how the heck could that work with an import statement?
58 |
59 | Later, I worked and experimented a lot in jupyter notebooks. Usually I would have some regular python module only with pure functions open in an editor, import that module in jupyter and have all the state there. Most of the time I'd spend in jupyter, but from time to time I'd edit the file and have to manually reload the module in jupyter - why couldn't it just automatically reload the module when my file is saved? Also, in the past reload() was a builtin function, now I have to import it extra from importlib, another hoop to jump through..
60 |
61 | Then there were situations where I found a cool module on github, a single file in a package I wanted to import - but why is it necessary to download it manually or maybe pip install an outdated package? Shouldn't it be possible to import just *this exact file* and somehow make it secure enough that I don't have to worry about man-in-the-middle attacks or some github-hack?
62 |
63 | On my journey I came across many blogposts, papers and presentation-notebooks with code that just 'import library' but mentions nowhere which actual version is being used, so trying to copy and paste this code in order to reproduce the presented experiment or solution is doomed to failure.
64 |
65 | I also remember how I had some code in a jupyter notebook that did 'import opencv' but I had not noted which actual version I had initially used. When I tried to run this notebook after a year again, it failed in a subtle way: the call signature of an opencv-function had slightly changed. It took quite a while to track down what my code was expecting and when this change occured until I figured out how to fix this issue. This could've been avoided or at least made a lot simpler if my imports were somehow annotated and checked in a pythonic way. After I complained about this in IRC, nedbat suggested an alternative functional way for imports with assignments: `mod = import("name", version)` which I found very alien and cumbersome at first sight - after all, we have an import statement for imports, and *there should be one and preferrably only one way to do it* - right?
66 |
67 | Well, those shortcomings of the import statement kept bugging me. And when I stumbled over the word 'use' as a name for whatever I was conceiving, I thought "what the heck, let's try it! - how hard could it be?!" Turns out, some challenges like actual, working hot-reloading are pretty hard! But by now use() can cover all the original usecases and there's even more to come!
68 |
69 | # Examples
70 | Here are a few tidbits on how to use() stuff to wet your appetite, for a more in-depth overview, check out our [Showcase](https://github.com/amogorkon/justuse/blob/unstable/docs/Showcase.ipynb)!
71 |
72 | `import use`
73 |
74 | `np = use("numpy", version="1.19.2")` corresponds to `import numpy as np; if np.version != "1.19.2": warn()`
75 |
76 | `use("pprint").pprint(some_dict)` corresponds to a one-off `from pprint import pprint; pprint(some_dict)` without importing it into the global namespace
77 |
78 | `tools = use(use.Path("/media/sf_Dropbox/code/tools.py"), reloading=True)` impossible to realize with classical imports
79 |
80 | `test = use("functions", initial_globals={"foo":34, "bar":49})` also impossible with the classical import statement, although importlib can help
81 |
82 | `utils = use(use.URL("https://raw.githubusercontent.com/PIA-Group/BioSPPy/7696d682dc3aafc898cd9161f946ea87db4fed7f/biosppy/utils.py"),
83 | hashes={"95f98f25ef8cfa0102642ea5babbe6dde3e3a19d411db9164af53a9b4cdcccd8"})` no chance with classical imports
84 |
85 | `np = use("numpy", version="1.21.0rc2", hashes={"3c90b0bb77615bda5e007cfa4c53eb6097ecc82e247726e0eb138fcda769b45d"}, modes=use.auto_install)` inline installation of packages and importing the same package with different versions in parallel in the same code - most people wouldn't even dream of that!
86 |
87 | ## There are strange chinese symbols in my hashes, am I being hacked?
88 | Nope. SHA256 hashes normally are pretty long (64 characters per hexdigest) and we require them defined within regular python code. Additionally, if you want to support multiple platforms, you need to supply a hash for every platform - which can add up to huge blocks of visual noise. Since Python 3 supports unicode by default, why not use the whole range of printable characters to encode those hashes? It's easier said than done - turns out emojis don't work well across different systems and editors - however, it *is* feasible to merge the Japanese, ASCII, Chinese and Korean alphabets into a single, big one we call JACK - which can be used to reliably encode those hashes in merely 18 characters. Since humans aren't supposed to manually type those hashes but simply copy&paste them anyway, there is only the question how to get them if you only have hexdigests at hand for some reason. Simply do `use.hexdigest_as_JACK(H)` and you're ready to go. Of course we also support classical hexdigests as fallback.
89 |
90 | ## Beware of Magic!
91 | Inspired by the q package/module, use() is a subclass of ModuleType, which is a callable class that replaces the module on import, so that only 'import use' is needed to be able to call use() on things.
92 |
93 | Probably the most magical thing about use() is that it does not return the plain module you wanted but a *ProxyModule* instead which adds a layer of abstraction. This allows things like automatic and transparent reloading without any intervention needed on your part. ProxyModules also add operations on modules like aspectizing via `mod @ (check, pattern, decorator)` syntax, which would not be possible with the classical import machinery.
94 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | codecov:
2 | require_ci_to_pass: false
3 | comment:
4 | behavior: default
5 | layout: reach,diff,flags,tree,reach
6 | show_carryforward_flags: true
7 | coverage:
8 | precision: 4
9 | range:
10 | - 0.0
11 | - 100.0
12 | round: up
13 | status:
14 | changes: false
15 | default_rules:
16 | flag_coverage_not_uploaded_behavior: exclude
17 | patch: false
18 | project: false
19 | github_checks:
20 | annotations: true
21 |
--------------------------------------------------------------------------------
/docs/codeflow.md:
--------------------------------------------------------------------------------
1 | This is an overflow of how justuse works. This should give you an idea where to look for things. Don't rely on this document.
2 |
3 | ## Initialization
4 |
5 | ```mermaid
6 | graph LR;
7 | A(__init__.py)-->B(main.py);
8 | B(main.py)-->C(instance of class Use replaces module use in __init__.py);
9 | ```
10 | ## Modes of operation
11 |
12 | This is defined on the class Use via singledispatch on the type of argument
13 | ```mermaid
14 | graph LR;
15 | A(use) --> B[\Path - module is a local file\]
16 | A(use) --> C[\URL - module is an online resource\]
17 | A(use) --> D[\string - module is part of a package, which may or may not be installed\]
18 | ```
19 |
20 | ## use package
21 | While using local or online resources as modules is very straight forward, using modules as part of packages is not.
22 | To use a module from a package with auto-installation, you need to think of the name to be used as two-parted. The first part is how you would pip-install it, the second is how you would import the module. You can specify the name in three ways:
23 |
24 | # package name that you would pip install = "py.foo"
25 | # module name that you would import = "foo.bar"
26 |
27 | use("py.foo/foo.bar")
28 | use(("py.foo", "foo.bar"))
29 | use(package_name="py.foo", module_name="foo.bar")
30 |
31 | However you call it, it all gets normalized into `name` (the single string representation), `package_name` for installation and `module_name` for import.
32 |
33 | ```mermaid
34 | graph TD
35 | A(main.Use._use_package) --> B[\normalization of all call-data into a dictionary of keyword args\]
36 | B --> C["buffet table" of functions, for dispatch on whichever combination of conditions]
37 | C --> D[\each function gets called with the same kwargs, picking the kwargs it needs, ignoring the rest\]
38 | ```
39 |
40 | ### auto-installation
41 | Inline-installation of packages is one of the most interesting and complex features of justuse.
42 |
43 | With version and hash properly defined and auto-installation requested, the flow of action is as following:
44 | ```mermaid
45 | graph TD
46 | A(main.Use._use_package) --> B[\pimp._auto_install\]
47 | B --> C{found in registry?}
48 | C -- yes --> D{zip?}
49 | D -- yes --> E[\try to import it directly via zipimport\]
50 | D -- no --> F[\try to install it using pip\]
51 | C -- no --> G[\download the artifact\]
52 | G --> D
53 | F --> H[import it via importlib]
54 | H --> I
55 | E --> I[return mod]
56 | ```
57 |
--------------------------------------------------------------------------------
/docs/database schema.md:
--------------------------------------------------------------------------------
1 | This is the schema of the registry database for reference.
2 |
3 | * Packages have a name and version, but are released as platform-dependent (or independent, as pure-python) distributions.
4 | * Each distribution corresponds to a single downloadable and installable file, called an artifact.
5 | * Each artifact has hashes that are generated using certain algorithms, which map to a very large number (no matter how this number is represented otherwise - as hexdigest or JACK or something else).
6 | * Once a distribution has been installed the artifact could be removed (since everything is now unpacked, compiled etc), but it also can be kept for further P2P sharing.
7 | * The distribution is installed in a venv, isolated.
8 |
9 | ```mermaid
10 |
11 | erDiagram
12 |
13 | artifacts {
14 | INTEGER id
15 | INTEGER distribution_id
16 | TEXT import_relpath
17 | TEXT artifact_path
18 | TEXT module_path
19 | }
20 |
21 | distributions {
22 | INTEGER id
23 | TEXT name
24 | TEXT version
25 | TEXT installation_path
26 | INTEGER date_of_installation
27 | INTEGER number_of_uses
28 | INTEGER date_of_last_use
29 | INTEGER pure_python_package
30 | }
31 |
32 | hashes {
33 | TEXT algo
34 | INTEGER value
35 | INTEGER artifact_id
36 | }
37 |
38 | hashes ||--o{ artifacts : "foreign key"
39 | artifacts ||--o{ distributions : "foreign key"
40 |
41 | ```
42 |
43 | # use(URL)
44 | In case of a single module being imported from a web-source, the module is cached in /web-modules as a file with a
45 | random but valid module name. We keep track of the mapping via DB: artifact.artifact_path -> web-URI used to fetch the
46 | module, artifact.module_path -> module-file
--------------------------------------------------------------------------------
/docs/demo.py:
--------------------------------------------------------------------------------
1 | def foo():
2 | print("Hello justuse-user!")
--------------------------------------------------------------------------------
/docs/module_a.py:
--------------------------------------------------------------------------------
1 | import use
2 |
3 | print("Hello from A!")
4 |
5 | use(use.Path("module_b.py"), initial_globals={"foo": 23})
--------------------------------------------------------------------------------
/docs/module_b.py:
--------------------------------------------------------------------------------
1 | foo: int
2 |
3 | print(f"Hello from B! foo={foo}")
4 |
--------------------------------------------------------------------------------
/docs/module_circular_a.py:
--------------------------------------------------------------------------------
1 | print("Hello from A!")
2 |
3 | import module_circular_b
4 |
5 | foo = 23
--------------------------------------------------------------------------------
/docs/module_circular_b.py:
--------------------------------------------------------------------------------
1 |
2 | from module_circular_a import foo
3 |
4 | print("Hello from B!", foo)
5 |
--------------------------------------------------------------------------------
/extra/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amogorkon/justuse/3a478fee76f728f681977ee8bf0738747317bc7f/extra/logo.png
--------------------------------------------------------------------------------
/extra/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
152 |
--------------------------------------------------------------------------------
/extra/web-message-mockup.pptx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amogorkon/justuse/3a478fee76f728f681977ee8bf0738747317bc7f/extra/web-message-mockup.pptx
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amogorkon/justuse/3a478fee76f728f681977ee8bf0738747317bc7f/logo.png
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | addopts = --continue-on-collection-errors --showlocals --color yes --code-highlight yes --verbose --import-mode=append -vv --log-cli-level ERROR --color yes
3 |
4 | testpaths =
5 | tests/unit_test.py
6 | tests/tdd_test.py
7 |
8 | env =
9 | PYTHONDEBUGMODE=0
10 |
11 | norecursedirs = tests/*
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | beartype >= 0.11.0
2 | furl ~= 2.1.2
3 | hypothesis ~= 6.23.1
4 | icontract ~= 2.5.4
5 | jinja2 ~= 3.1.2
6 | packaging == 21.0
7 | pip
8 | pydantic >= 1.8.2
9 | pytest ~= 6.2.4
10 | pytest-cov ~= 2.12.1
11 | pytest-env ~= 0.6.2
12 | wheel >= 0.38 # snyk security warning for <0.38
13 | urllib3>=2.2.2 # not directly required, pinned by Snyk to avoid a vulnerability
14 | requests >= 2.24.0
15 | tomli; python_version <= '3.11'
16 | zipp>=3.19.1 # not directly required, pinned by Snyk to avoid a vulnerability
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | from pathlib import Path
3 |
4 | from setuptools import find_packages, setup
5 |
6 | here = os.path.abspath(os.path.dirname(__file__))
7 | src = os.path.join(here, "src/use")
8 |
9 | # Instead of doing the obvious thing (importing 'use' directly and just reading '__version__'),
10 | # we are parsing the version out of the source AST here, because if the user is missing any
11 | # dependencies at setup time, an import error would prevent the installation.
12 | # Two simple ways to verify the installation using this setup.py file:
13 | #
14 | # python3 setup.py develop
15 | #
16 | # or:
17 | #
18 | # python3 setup.py install
19 | #
20 | import ast
21 |
22 | with open(os.path.join(src, "__init__.py")) as f:
23 | mod = ast.parse(f.read())
24 | version = [
25 | t
26 | for t in [*filter(lambda n: isinstance(n, ast.Assign), mod.body)]
27 | if t.targets[0].id == "__version__"
28 | ][0].value.value
29 |
30 | meta = {
31 | "name": "justuse",
32 | "license": "MIT",
33 | "url": "https://github.com/amogorkon/justuse",
34 | "version": version,
35 | "author": "Anselm Kiefner",
36 | "author_email": "justuse-pypi@anselm.kiefner.de",
37 | "python_requires": ">=3.12",
38 | "keywords": [
39 | "installing",
40 | "packages",
41 | "hot reload",
42 | "auto install",
43 | "aspect oriented",
44 | "version checking",
45 | "functional",
46 | ],
47 | "classifiers": [
48 | "Development Status :: 4 - Beta",
49 | "Intended Audience :: Developers",
50 | "Natural Language :: English",
51 | "License :: OSI Approved :: MIT License",
52 | "Operating System :: OS Independent",
53 | "Programming Language :: Python :: 3 :: Only",
54 | ],
55 | "extras_require": {"test": ["pytest", "pytest-cov", "pytest-env"]},
56 | "fullname": "justuse",
57 | "dist_files": ["pytest.ini", "tests/pytest.ini"],
58 | "description": "a pure-python alternative to import",
59 | "maintainer_email": "justuse-pypi@anselm.kiefner.de",
60 | "maintainer": "Anselm Kiefner",
61 | "platforms": ["any"],
62 | "download_url": "https://github.com/amogorkon/justuse/"
63 | "archive/refs/heads/main.zip",
64 | }
65 |
66 |
67 | requires = (
68 | "beartype( >= 0.18.5)",
69 | "furl(>= 2.1.3)",
70 | "icontract(>= 2.6.6)",
71 | "jinja2",
72 | "packaging(== 24.1)",
73 | "pip",
74 | "pydantic(>= 2.8.2)",
75 | "requests",
76 | "wheel",
77 | )
78 |
79 |
80 | LONG_DESCRIPTION = Path("README.md").read_text()
81 | setup(
82 | packages=find_packages(where="src"),
83 | package_dir={
84 | "": "src",
85 | },
86 | package_name="use",
87 | include_package_data=True,
88 | long_description=LONG_DESCRIPTION,
89 | long_description_content_type="text/markdown",
90 | requires=requires,
91 | install_requires=requires,
92 | setup_requires=requires,
93 | zip_safe=True,
94 | **meta,
95 | )
96 |
--------------------------------------------------------------------------------
/src/.test4.py:
--------------------------------------------------------------------------------
1 | from time import perf_counter_ns
2 | from collections import deque, defaultdict
3 | from functools import wraps
4 | from typing import DefaultDict, Deque
5 |
6 | from time import perf_counter_ns as time
7 | from collections import deque, defaultdict
8 |
9 | _timings: dict[int, Deque[int]] = DefaultDict(lambda: deque(maxlen=10))
10 |
11 |
12 | def tinny_profiler(func: callable) -> callable:
13 | """
14 | Decorator to log/track/debug calls and results.
15 |
16 | Args:
17 | func (function): The function to decorate.
18 | Returns:
19 | function: The decorated callable.
20 | """
21 |
22 | @wraps(func)
23 | def wrapper(*args, **kwargs):
24 | before = perf_counter_ns()
25 | res = func(*args, **kwargs)
26 | after = perf_counter_ns()
27 | _timings[func].append(after - before)
28 | return res
29 |
30 | return wrapper
31 |
32 | @tinny_profiler
33 | def a():
34 | print("adsf")
35 |
36 | a()
--------------------------------------------------------------------------------
/src/.test5.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import time
3 | import logging
4 | import use
5 | from pathlib import Path
6 |
7 | Observer = use('watchdog.observers', version='2.3.0', modes=use.auto_install, hash_algo=use.Hash.sha256, hashes={
8 | 'Q鉨麲㺝翪峬夛冕廛䀳迆婃儈正㛣辐Ǵ娇', # py3-win_amd64
9 | }).Observer
10 |
11 | LoggingEventHandler = use('watchdog.events', version='2.3.0', modes=use.auto_install, hash_algo=use.Hash.sha256, hashes={
12 | 'Q鉨麲㺝翪峬夛冕廛䀳迆婃儈正㛣辐Ǵ娇', # py3-win_amd64
13 | }).LoggingEventHandler
14 |
15 |
16 | logging.basicConfig(level=logging.INFO,
17 | format='%(asctime)s - %(message)s',
18 | datefmt='%Y-%m-%d %H:%M:%S')
19 | path = Path(r"E:\Dropbox\code\_test")
20 | event_handler = LoggingEventHandler()
21 | observer = Observer()
22 | observer.schedule(event_handler, path)
23 | observer.start()
24 | try:
25 | while True:
26 | time.sleep(1)
27 | finally:
28 | observer.stop()
29 | observer.join()
30 |
--------------------------------------------------------------------------------
/src/.test6.py:
--------------------------------------------------------------------------------
1 | import use
2 |
3 | use('thefuzz', version='0.19', modes=use.auto_install, hash_algo=use.Hash.sha256, hashes={
4 | 'N頓盥㳍藿鞡傫韨司䧷焲瘴䵉紅蚥鲅獳陷', # py2.py3-any
5 | })
6 | from thefuzz import fuzz
7 | from thefuzz import process
--------------------------------------------------------------------------------
/src/.test7.py:
--------------------------------------------------------------------------------
1 | class Test:
2 | def __matmul__(self, other):
3 | print("matmul", other)
4 |
5 | t = Test()
--------------------------------------------------------------------------------
/src/.test8.py:
--------------------------------------------------------------------------------
1 | def get_string_representation_of_dict_without_quotation_marks(d: dict) -> str:
2 | return str(d).replace("'", '')
3 |
4 | d = {1:2, 3:4}
5 | print(f"{ get_string_representation_of_dict_without_quotation_marks(d)}")
--------------------------------------------------------------------------------
/src/.test9.py:
--------------------------------------------------------------------------------
1 | tuples = (1, 2), (23, 4)
2 |
3 |
4 | def f(x):
5 | return x * 2
6 |
--------------------------------------------------------------------------------
/src/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | addopts = --continue-on-collection-errors --showlocals --color yes --code-highlight yes --import-mode=append -vv
3 |
4 | testpaths =
5 | ../tests/unit_test.py
6 | ../tests/tdd_test.py
7 |
8 | env =
9 | PYTHONDEBUGMODE=0
10 |
--------------------------------------------------------------------------------
/src/testfile.whl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amogorkon/justuse/3a478fee76f728f681977ee8bf0738747317bc7f/src/testfile.whl
--------------------------------------------------------------------------------
/src/use/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | This is where the story begins. Welcome to JustUse!
3 | Only imports and project-global constants are defined here.
4 | All superfluous imports are deleted to clean up the namespace - and thus help()
5 |
6 | """
7 |
8 | import os
9 | import sys
10 | import tempfile
11 | from datetime import datetime, timezone
12 | from enum import Enum, IntEnum
13 | from logging import basicConfig, getLogger
14 | from pathlib import Path
15 | from uuid import uuid4
16 | from warnings import catch_warnings, filterwarnings
17 |
18 | from beartype import beartype
19 | from beartype.roar import BeartypeDecorHintPep585DeprecationWarning
20 |
21 | sessionID = uuid4()
22 | del uuid4
23 |
24 | home = Path(os.getenv("JUSTUSE_HOME", str(Path.home() / ".justuse-python")))
25 |
26 | try:
27 | home.mkdir(mode=0o755, parents=True, exist_ok=True)
28 | except PermissionError:
29 | # this should fix the permission issues on android #80
30 | home = tempfile.mkdtemp(prefix="justuse_")
31 |
32 | if sys.version_info < (3, 11):
33 | import tomli as toml
34 | else:
35 | import tomllib as toml
36 |
37 | (home / "config.toml").touch(mode=0o755, exist_ok=True)
38 |
39 | with open(home / "config.toml", "rb") as f:
40 | config_dict = toml.load(f)
41 | del toml
42 |
43 | from use.pydantics import Configuration
44 |
45 | config = Configuration(**config_dict)
46 | del Configuration
47 |
48 | config.logs.mkdir(mode=0o755, parents=True, exist_ok=True)
49 | config.packages.mkdir(mode=0o755, parents=True, exist_ok=True)
50 | config.web_modules.mkdir(mode=0o755, parents=True, exist_ok=True)
51 |
52 | for file in (
53 | config.logs / "usage.log",
54 | config.registry,
55 | ):
56 | file.touch(mode=0o755, exist_ok=True)
57 |
58 |
59 | def fraction_of_day(now: datetime = None) -> float:
60 | if now is None:
61 | now = datetime.now(timezone.utc)
62 | return round(
63 | (
64 | now.hour / 24
65 | + now.minute / (24 * 60)
66 | + now.second / (24 * 60 * 60)
67 | + now.microsecond / (24 * 60 * 60 * 1000 * 1000)
68 | )
69 | * 1000,
70 | 6,
71 | )
72 |
73 |
74 | # sourcery skip: avoid-builtin-shadow
75 | basicConfig(
76 | filename=config.logs / "usage.log",
77 | filemode="a",
78 | encoding="UTF-8", #
79 | format="%(asctime)s %(levelname)s %(name)s %(message)s",
80 | datefmt=f"%Y%m%d {fraction_of_day()}",
81 | # datefmt="%Y-%m-%d %H:%M:%S",
82 | level=config.debug_level,
83 | )
84 |
85 | # !!! SEE NOTE !!!
86 | # IMPORTANT; The setup.py script must be able to read the
87 | # current use __version__ variable **AS A STRING LITERAL** from
88 | # this file. If you do anything except updating the version,
89 | # please check that setup.py can still be executed.
90 | __version__ = "0.9.1.1.0"
91 | # for tests
92 | __version__ = os.getenv("USE_VERSION", __version__)
93 | __name__ = "use"
94 | __package__ = "use"
95 |
96 | log = getLogger(__name__)
97 |
98 | import use.logutil
99 |
100 |
101 | class JustuseIssue:
102 | pass
103 |
104 |
105 | class NirvanaWarning(Warning, JustuseIssue):
106 | pass
107 |
108 |
109 | class VersionWarning(Warning, JustuseIssue):
110 | pass
111 |
112 |
113 | class NotReloadableWarning(Warning, JustuseIssue):
114 | pass
115 |
116 |
117 | class NoValidationWarning(Warning, JustuseIssue):
118 | pass
119 |
120 |
121 | class AmbiguityWarning(Warning, JustuseIssue):
122 | pass
123 |
124 |
125 | class UnexpectedHash(ImportError, JustuseIssue):
126 | pass
127 |
128 |
129 | class InstallationError(ImportError, JustuseIssue):
130 | pass
131 |
132 |
133 | # Coerce all PEP 585 deprecation warnings into fatal exceptions.
134 | with catch_warnings():
135 | from beartype.roar import BeartypeDecorHintPep585DeprecationWarning
136 |
137 | filterwarnings(
138 | "ignore", category=BeartypeDecorHintPep585DeprecationWarning, module="beartype"
139 | )
140 |
141 |
142 | import hashlib
143 |
144 |
145 | class Hash(Enum):
146 | sha256 = hashlib.sha256
147 | blake = hashlib.blake2s
148 |
149 |
150 | del hashlib
151 |
152 |
153 | class Modes(IntEnum):
154 | auto_install = 2**0
155 | fatal_exceptions = 2**1
156 | reloading = 2**2
157 | no_public_installation = 2**3
158 | fastfail = 2**4
159 | recklessness = 2**5
160 | no_browser = 2**6
161 | no_cleanup = 2**7
162 |
163 |
164 | from use.aspectizing import apply as apply
165 | from use.aspectizing import apply_aspect
166 | from use.aspectizing import iter_submodules as iter_submodules
167 | from use.aspectizing import show_aspects as show_aspects
168 | from use.aspectizing import show_profiling as show_profiling
169 | from use.aspectizing import tinny_profiler as tinny_profiler
170 | from use.aspectizing import woody_logger as woody_logger
171 | from use.buffet import buffet_table as buffet_table
172 | from use.main import URL as URL
173 | from use.main import ProxyModule, Use, test_version
174 | from use.pydantics import Version as Version
175 | from use.pydantics import git as git
176 |
177 | for member in Modes:
178 | setattr(Use, member.name, member.value)
179 |
180 | use = Use()
181 |
182 | del os
183 | use.__dict__.update(dict(globals()))
184 | use = ProxyModule(use)
185 | if not test_version:
186 | sys.modules["use"] = use
187 |
188 | del test_version
189 | del use.__dict__["test_version"]
190 | del sys
191 | del use.__dict__["sys"]
192 |
193 | with catch_warnings():
194 | filterwarnings(
195 | "ignore", category=BeartypeDecorHintPep585DeprecationWarning, module="beartype"
196 | )
197 | apply_aspect(use.iter_submodules(use), beartype)
198 |
--------------------------------------------------------------------------------
/src/use/aspectizing.py:
--------------------------------------------------------------------------------
1 | import ast
2 | import contextlib
3 | import inspect
4 | import re
5 | import sys
6 | from collections import namedtuple
7 | from collections.abc import Callable, Iterable, Sized
8 | from functools import wraps
9 | from logging import getLogger
10 | from pathlib import Path
11 | from time import perf_counter_ns
12 | from types import ModuleType
13 | from typing import Any, DefaultDict, Deque, Optional, Union
14 |
15 | from beartype import beartype
16 |
17 | log = getLogger(__name__)
18 |
19 | from use import config
20 | from use.messages import _web_aspectized_dry_run, _web_tinny_profiler
21 |
22 | # TODO: use an extra WeakKeyDict as watchdog for object deletions and trigger cleanup in these here
23 | _applied_decorators: DefaultDict[tuple[object, str], Deque[Callable]] = DefaultDict(
24 | Deque
25 | )
26 | "to see which decorators are applied, in which order"
27 | _aspectized_functions: DefaultDict[tuple[object, str], Deque[Callable]] = DefaultDict(
28 | Deque
29 | )
30 | "the actually decorated functions to undo aspectizing"
31 |
32 |
33 | def show_aspects():
34 | """Open a browser to properly display all the things that have been aspectized thus far."""
35 | print("decorators:", _applied_decorators)
36 | print("functions:", _aspectized_functions)
37 | # _web_aspectized(_applied_decorators, _aspectized_functions)
38 |
39 |
40 | def is_callable(thing):
41 | try:
42 | return callable(
43 | object.__getattribute__(type(thing), "__call__")
44 | ) # to even catch weird non-callable __call__ methods
45 | except AttributeError:
46 | return False
47 |
48 |
49 | HIT = namedtuple("Hit", "qualname name type success exception dunder mod_name")
50 |
51 |
52 | def really_callable(thing):
53 | try:
54 | thing.__get__
55 | thing.__call__
56 | return True
57 | except Exception:
58 | return False
59 |
60 |
61 | def apply_aspect(
62 | thing: Union[object, Iterable[object]],
63 | decorator: Callable,
64 | /,
65 | *,
66 | check: Callable = is_callable,
67 | dry_run: bool = False,
68 | pattern: str = "",
69 | excluded_names: Optional[set[str]] = None,
70 | excluded_types: Optional[set[type]] = None,
71 | file=None,
72 | ) -> None:
73 | """Apply the aspect as a side-effect, no copy is created."""
74 | regex = re.compile(pattern, re.DOTALL)
75 |
76 | if excluded_names is None:
77 | excluded_names = set()
78 | if excluded_types is None:
79 | excluded_types = set()
80 |
81 | visited = {id(obj) for obj in vars(object).values()}
82 | visited.add(id(type))
83 |
84 | hits = []
85 |
86 | def aspectize(
87 | thing: Any,
88 | decorator: Callable,
89 | /,
90 | *,
91 | qualname_lst: Optional[list] = None,
92 | mod_name: str,
93 | ) -> Iterable[HIT]:
94 | name = getattr(thing, "__name__", str(thing))
95 | if not qualname_lst:
96 | qualname_lst = []
97 | if id(thing) in visited:
98 | return
99 |
100 | qualname_lst.append(name)
101 |
102 | # let's stick within the module boundary
103 | try:
104 | thing_mod_name = inspect.getmodule(thing).__name__
105 | except AttributeError:
106 | thing_mod_name = ""
107 | if thing_mod_name != mod_name:
108 | return
109 |
110 | for name in dir(thing):
111 | qualname = (
112 | ""
113 | if len(qualname_lst) < 3
114 | else "..." + ".".join(qualname_lst[-3:]) + "." + name
115 | )
116 | obj = getattr(thing, name, None)
117 | qualname = getattr(obj, "__qualname__", qualname)
118 |
119 | if obj is None:
120 | continue
121 | # Time to get serious!
122 |
123 | if type(obj) == type:
124 | aspectize(obj, decorator, qualname_lst=qualname_lst, mod_name=mod_name)
125 |
126 | if id(obj) in visited:
127 | continue
128 |
129 | if (
130 | qualname in excluded_names
131 | or type(obj) in excluded_types
132 | or not check(obj)
133 | or not regex.match(name)
134 | ):
135 | continue
136 |
137 | msg = ""
138 | success = True
139 |
140 | try:
141 | wrapped = _wrap(thing=thing, obj=obj, decorator=decorator, name=name)
142 | if dry_run:
143 | _unwrap(thing=thing, name=name)
144 | except BaseException as exc:
145 | wrapped = obj
146 | success = False
147 | msg = str(exc)
148 | if file:
149 | print(msg, file=file)
150 | assert isinstance(name, str) and len(name) > 0
151 | assert isinstance(mod_name, str) and mod_name != ""
152 |
153 | hits.append(
154 | HIT(
155 | qualname,
156 | name,
157 | type(wrapped),
158 | success,
159 | msg,
160 | name.startswith("__") and name.endswith("__"),
161 | mod_name,
162 | )
163 | )
164 | visited.add(id(wrapped))
165 |
166 | def call(m):
167 | try:
168 | mod_name = inspect.getmodule(m).__name__
169 | except AttributeError:
170 | mod_name = ""
171 | aspectize(m, decorator, mod_name=mod_name)
172 |
173 | if isinstance(thing, Iterable):
174 | for x in thing:
175 | call(x)
176 | else:
177 | call(thing)
178 |
179 | if dry_run:
180 | if config.no_browser:
181 | print(
182 | "Tried to do a dry run and display the results, but no_browser is set in config."
183 | )
184 | else:
185 | print(
186 | "Please check your browser to select options and filters for aspects."
187 | )
188 | log.warning(
189 | "Please check your browser to select options and filters for aspects."
190 | )
191 | _web_aspectized_dry_run(
192 | decorator=decorator,
193 | pattern=pattern,
194 | check=check,
195 | hits=hits,
196 | mod_name=str(thing),
197 | )
198 |
199 |
200 | @beartype
201 | def _wrap(*, thing: Any, obj: Any, decorator: Callable, name: str) -> Any:
202 | wrapped = decorator(obj)
203 | _applied_decorators[(thing, name)].append(decorator)
204 | _aspectized_functions[(thing, name)].append(obj)
205 |
206 | # This will fail with TypeError on built-in/extension types.
207 | # We handle exceptions outside, let's not betray ourselves.
208 | setattr(thing, name, wrapped)
209 | return wrapped
210 |
211 |
212 | def apply(*, thing, decorator, name):
213 | """Public version of aspectizing a single thing."""
214 | obj = getattr(thing, name)
215 | wrapped = decorator(obj)
216 | setattr(thing, name, wrapped)
217 | return wrapped
218 |
219 |
220 | @beartype
221 | def _unwrap(*, thing: Any, name: str):
222 | try:
223 | original = _aspectized_functions[(thing, name)].pop()
224 | except IndexError:
225 | del _aspectized_functions[(thing, name)]
226 | original = getattr(thing, name)
227 |
228 | try:
229 | _applied_decorators[(thing, name)].pop()
230 | except IndexError:
231 | del _applied_decorators[(thing, name)]
232 | setattr(thing, name, original)
233 |
234 | return original
235 |
236 |
237 | def _qualname(thing):
238 | if type(thing) is bool:
239 | return str(thing)
240 | module = getattr(thing, "__module__", None) or getattr(
241 | thing.__class__, "__module__", None
242 | )
243 | qualname = (
244 | getattr(thing, "__qualname__", None)
245 | or getattr(thing, "__name__", None)
246 | or f"{type(thing).__name__}()"
247 | )
248 | qualname = destringified(qualname)
249 | return qualname if module == "builtins" else f"{module}::{qualname}"
250 |
251 |
252 | def destringified(thing):
253 | return str(thing).replace("'", "")
254 |
255 |
256 | def describe(thing):
257 | if thing is None:
258 | return "None"
259 | if type(thing) is bool:
260 | return str(thing)
261 | if isinstance(thing, (Sized)):
262 | if len(thing) == 0:
263 | return f"{_qualname(type(thing))} (empty)"
264 | if len(thing) < 4:
265 | return f"{_qualname(type(thing))}{destringified([_qualname(x) for x in thing])}]"
266 | else:
267 | return f"{_qualname(type(thing))}[{_qualname(type(thing[0]))}] ({len(thing)} items)"
268 | if isinstance(thing, (Iterable)):
269 | return f"{_qualname(type(thing))} (content indeterminable)"
270 | return _qualname(thing)
271 |
272 |
273 | def woody_logger(thing: Callable) -> Callable:
274 | """
275 | Decorator to log/track/debug calls and results.
276 |
277 | Args:
278 | func (function): The function to decorate.
279 | Returns:
280 | function: The decorated callable.
281 | """
282 | # A class is an instance of type - its type is also type, but only checking for `type(thing) is type`
283 | # would exclude metaclasses other than type - not complicated at all.
284 | name = _qualname(thing)
285 |
286 | if isinstance(thing, type):
287 | # wrapping the class means wrapping `__new__` mainly, which must return the original class, not just a proxy -
288 | # because a proxy never could hold up under scrutiny of type(), which can't be faked (like, at all).
289 | class wrapper(thing.__class__):
290 | def __new__(cls, *args, **kwargs):
291 | # let's check who's calling us
292 | caller = inspect.currentframe().f_back.f_code.co_name
293 | print(f"{caller}({args} {kwargs}) -> {name}()")
294 | before = perf_counter_ns()
295 | res = thing(*args, **kwargs)
296 | after = perf_counter_ns()
297 | print(
298 | f"-> {name}() (in {after - before} ns ({round((after - before) / 10** 9, 5)} sec) -> {type(res)}",
299 | sep="\n",
300 | )
301 | return res
302 |
303 | else:
304 | # Wrapping a function is way easier.. except..
305 | @wraps(thing)
306 | def wrapper(*args, **kwargs):
307 | # let's check who's calling us
308 | caller = inspect.currentframe().f_back.f_code
309 | if caller.co_name == "":
310 | caller = Path(caller.co_filename).name
311 | else:
312 | caller = f"{Path(caller.co_filename).name}::{caller.co_name}"
313 | print(
314 | f"{caller}([{', '.join(describe(a) for a in args)}] {destringified({k: describe(v) for k, v in kwargs.items()}) if len(kwargs) < 4 else f'dict of len{len(kwargs)}'}) -> {_qualname(thing)}"
315 | )
316 | before = perf_counter_ns()
317 | res = thing(*args, **kwargs)
318 | after = perf_counter_ns()
319 | if isinstance(res, (Sized)):
320 | print(
321 | f"-> {describe(thing)} (in {after - before} ns ({round((after - before) / 10** 9, 5)} sec) -> {describe(res)}",
322 | sep="\n",
323 | )
324 | return res
325 | if isinstance(
326 | res, (Iterable)
327 | ): # TODO: Iterable? Iterator? Generator? Ahhhh!
328 | print(
329 | f"-> {describe(thing)} (in {after - before} ns ({round((after - before) / 10** 9, 5)} sec) -> {describe(res)}",
330 | sep="\n",
331 | )
332 | return res
333 |
334 | print(
335 | f"-> {describe(thing)} (in {after - before} ns ({round((after - before) / 10** 9, 5)} sec) -> {describe(res)}",
336 | sep="\n",
337 | )
338 | return res
339 |
340 | return wrapper
341 |
342 |
343 | _timings: dict[int, Deque[int]] = DefaultDict(lambda: Deque(maxlen=10000))
344 |
345 |
346 | def tinny_profiler(func: callable) -> callable:
347 | """
348 | Decorator to log/track/debug calls and results.
349 |
350 | Args:
351 | func (function): The function to decorate.
352 | Returns:
353 | function: The decorated callable.
354 | """
355 |
356 | @wraps(func)
357 | def wrapper(*args, **kwargs):
358 | before = perf_counter_ns()
359 | res = func(*args, **kwargs)
360 | after = perf_counter_ns()
361 | _timings[func].append(after - before)
362 | return res
363 |
364 | return wrapper
365 |
366 |
367 | def show_profiling():
368 | _web_tinny_profiler(_timings)
369 |
370 |
371 | def _is_builtin(name, mod):
372 | if name in sys.builtin_module_names:
373 | return True
374 |
375 | if hasattr(mod, "__file__"):
376 | relpath = Path(mod.__file__).parent.relative_to(
377 | (Path(sys.executable).parent / "lib")
378 | )
379 | if relpath == Path():
380 | return True
381 | if relpath.parts[0] == "site-packages":
382 | return False
383 | return True
384 |
385 |
386 | def _get_imports_from_module(mod):
387 | if not hasattr(mod, "__file__"):
388 | return
389 | with open(mod.__file__, "rb") as file:
390 | with contextlib.suppress(ValueError):
391 | for x in ast.walk(ast.parse(file.read())):
392 | if isinstance(x, ast.Import):
393 | name = x.names[0].name
394 | if (mod := sys.modules.get(name)) and not _is_builtin(name, mod):
395 | yield name, mod
396 | if isinstance(x, ast.ImportFrom):
397 | name = x.module
398 | if (mod := sys.modules.get(name)) and not _is_builtin(name, mod):
399 | yield name, mod
400 |
401 |
402 | def iter_submodules(mod: ModuleType, visited=None, results=None) -> set[ModuleType]:
403 | """Find all modules recursively that were imported as dependency from the given module."""
404 | if results is None:
405 | results = set()
406 | if visited is None:
407 | visited = set()
408 | for name, x in _get_imports_from_module(mod):
409 | if name in visited:
410 | continue
411 | visited.add(name)
412 | results.add(name)
413 | for name, x in _get_imports_from_module(sys.modules[name]):
414 | results.add(x)
415 | iter_submodules(x, visited, results)
416 | return results
417 |
--------------------------------------------------------------------------------
/src/use/buffet.py:
--------------------------------------------------------------------------------
1 | # noqa: E701
2 | # here we're building the buffet of the future with pattern matching (>=3.10)
3 |
4 | from logging import getLogger
5 |
6 | from use.messages import UserMessage as Message
7 | from use.pimp import (
8 | _auto_install,
9 | _ensure_version,
10 | _import_public_no_install,
11 | _pebkac_no_hash,
12 | _pebkac_no_version,
13 | _pebkac_no_version_no_hash,
14 | )
15 | from use.tools import pipes
16 |
17 | log = getLogger(__name__)
18 |
19 |
20 | # fmt: off
21 | @pipes
22 | def buffet_table(case, kwargs):
23 | match case:
24 | # +-------------------------- version specified?
25 | # | +----------------------- hash specified?
26 | # | | +-------------------- publicly available?
27 | # | | | +----------------- auto-install requested?
28 | # | | | |
29 | # v v v v
30 | case 1, 1, 1, 1: return _import_public_no_install(**kwargs) >> _ensure_version(**kwargs) >> _auto_install(**kwargs) # noqa: E701
31 | case 1, 1, 0, 1: return _auto_install(**kwargs) # noqa: E701
32 | case 1, _, 1, 0: return _import_public_no_install(**kwargs) >> _ensure_version(**kwargs) # noqa: E701
33 | case 0, 0, _, 1: return _pebkac_no_version_no_hash(**kwargs) # noqa: E701
34 | case 1, 0, _, 1: return _pebkac_no_hash(**kwargs) # noqa: E701
35 | case 0, 1, _, 1: return _pebkac_no_version(**kwargs) # noqa: E701
36 | case 0, _, 1, 0: return _import_public_no_install(**kwargs) # noqa: E701
37 | case _, _, 0, 0: return ImportError(Message.cant_import(kwargs["pkg_name"])) # noqa: E701
38 | # fmt: on
39 |
--------------------------------------------------------------------------------
/src/use/buffet_old.py:
--------------------------------------------------------------------------------
1 | """
2 | Buffet pattern.
3 | Basically dispatch on a specific case, pass in all local context from which the function takes what it needs.
4 | """
5 |
6 | from logging import getLogger
7 |
8 | log = getLogger(__name__)
9 |
10 | from use import pimp
11 | from use.messages import UserMessage as Message
12 |
13 | # the buffet of the past (<3.10)
14 |
15 | # fmt: off
16 | def buffet_table(case, kwargs):
17 | name = kwargs.get('name')
18 | case_func = {
19 | (0, 0, 0, 0): lambda: ImportError(Message.cant_import(name)),
20 | (0, 0, 0, 1): lambda: pimp._pebkac_no_version_no_hash(**kwargs),
21 | (0, 0, 1, 0): lambda: pimp._import_public_no_install(**kwargs),
22 | (0, 1, 0, 0): lambda: ImportError(Message.cant_import(name)),
23 | (1, 0, 0, 0): lambda: ImportError(Message.cant_import(name)),
24 | (0, 0, 1, 1): lambda: pimp._pebkac_no_version_no_hash(**kwargs),
25 | (0, 1, 1, 0): lambda: pimp._import_public_no_install(**kwargs),
26 | (1, 1, 0, 0): lambda: ImportError(Message.cant_import(name)),
27 | (1, 0, 0, 1): lambda: pimp._pebkac_no_hash(**kwargs),
28 | (1, 0, 1, 0): lambda: pimp._ensure_version(pimp._import_public_no_install(**kwargs), **kwargs),
29 | (0, 1, 0, 1): lambda: pimp._pebkac_no_version(**kwargs),
30 | (0, 1, 1, 1): lambda: pimp._pebkac_no_version(**kwargs),
31 | (1, 0, 1, 1): lambda: pimp._pebkac_no_hash(**kwargs),
32 | (1, 1, 0, 1): lambda: pimp._auto_install(**kwargs),
33 | (1, 1, 1, 0): lambda: pimp._ensure_version(pimp._import_public_no_install(**kwargs), **kwargs),
34 | (1, 1, 1, 1): lambda: pimp._auto_install(
35 | func=lambda: pimp._ensure_version(pimp._import_public_no_install(**kwargs), **kwargs), **kwargs
36 | ),
37 | }[case]
38 | result = case_func()
39 | log.info("result = %s", repr(result))
40 | return result
41 | # fmt: on
42 |
--------------------------------------------------------------------------------
/src/use/logutil.py:
--------------------------------------------------------------------------------
1 | import io
2 | import logging
3 | import os
4 | import sys
5 | import time
6 | import traceback
7 | from logging import Formatter, PercentStyle, StreamHandler, StrFormatStyle, StringTemplateStyle
8 | from typing import NamedTuple, Deque
9 | from collections.abc import Callable
10 |
11 | import use
12 |
13 | BASIC_FORMAT: str = "%(levelname)s:%(name)s:%(message)s"
14 |
15 | _STYLES: dict[str, tuple[PercentStyle, str]] = {
16 | "%": (PercentStyle, BASIC_FORMAT),
17 | "{": (StrFormatStyle, "{levelname}:{name}:{message}"),
18 | "$": (
19 | StringTemplateStyle,
20 | "\x1b[1;3${levelno}m${levelname}\x1b[1;30m: \x1b[0m\x1b[1;36m${name}\x1b[1;30m: \x1b[0m${message}",
21 | ),
22 | }
23 |
24 |
25 | class TimeResult(
26 | NamedTuple(
27 | "TimeResult",
28 | tm_year=int,
29 | tm_mon=int,
30 | tm_mday=int,
31 | tm_hour=int,
32 | tm_min=int,
33 | tm_sec=int,
34 | tm_wday=int,
35 | tm_yday=int,
36 | tm_isdst=bool,
37 | )
38 | ):
39 | pass
40 |
41 |
42 | class ConsoleFormatter(Formatter):
43 | """
44 | Formatter instances are used to convert a LogRecord to text.
45 |
46 | Formatters need to know how a LogRecord is constructed. They are
47 | responsible for converting a LogRecord to (usually) a string which can
48 | be interpreted by either a human or an external system. The base Formatter
49 | allows a formatting string to be specified. If none is supplied, the
50 | style-dependent default value, "%(message)s", "{message}", or
51 | "${message}", is used.
52 |
53 | The Formatter can be initialized with a format string which makes use of
54 | knowledge of the LogRecord attributes - e.g. the default value mentioned
55 | above makes use of the fact that the user's message and arguments are pre-
56 | formatted into a LogRecord's message attribute. Currently, the useful
57 | attributes in a LogRecord are described by:
58 |
59 | %(name)s Name of the logger (logging channel)
60 | %(levelno)s Numeric logging level for the message (DEBUG, INFO,
61 | WARNING, ERROR, CRITICAL)
62 | %(levelname)s Text logging level for the message ("DEBUG", "INFO",
63 | "WARNING", "ERROR", "CRITICAL")
64 | %(pathname)s Full pathname of the source file where the logging
65 | call was issued (if available)
66 | %(filename)s Filename portion of pathname
67 | %(module)s Module (name portion of filename)
68 | %(lineno)d Source line number where the logging call was issued
69 | (if available)
70 | %(funcName)s Function name
71 | %(created)f Time when the LogRecord was created (time.time()
72 | return value)
73 | %(asctime)s Textual time when the LogRecord was created
74 | %(msecs)d Millisecond portion of the creation time
75 | %(relativeCreated)d Time in milliseconds when the LogRecord was created,
76 | relative to the time the logging module was loaded
77 | (typically at application startup time)
78 | %(thread)d Thread ID (if available)
79 | %(threadName)s Thread name (if available)
80 | %(process)d Process ID (if available)
81 | %(message)s The result of record.getMessage(), computed just as
82 | the record is emitted
83 | """
84 |
85 | converter: Callable[[int], TimeResult]
86 | style: tuple[PercentStyle, str]
87 | default_time_format: str = "%Y-%m-%d %H:%M:%S"
88 | default_msec_format: str = "%s,%03d"
89 | level = property(lambda self: logging.root.level)
90 |
91 | def __init__(self):
92 | """
93 | Initialize the formatter
94 | """
95 | self.datefmt = "%Y-%m-%d"
96 | style = "$"
97 | fmt = _STYLES[style][1]
98 | super(ConsoleFormatter, self).__init__(fmt=fmt, datefmt=self.datefmt, style=style, validate=True)
99 |
100 | def formatTime(self, record, datefmt=None):
101 | """
102 | Return the creation time of the specified LogRecord as formatted text.
103 |
104 | This method should be called from format() by a formatter which
105 | wants to make use of a formatted time. This method can be overridden
106 | in formatters to provide for any specific requirement, but the
107 | basic behaviour is as follows: if datefmt (a string) is specified,
108 | it is used with time.strftime() to format the creation time of the
109 | record. Otherwise, an ISO8601-like (or RFC 3339-like) format is used.
110 | The resulting string is returned. This function uses a user-configurable
111 | function to convert the creation time to a tuple. By default,
112 | time.localtime() is used; to change this for a particular formatter
113 | instance, set the 'converter' attribute to a function with the same
114 | signature as time.localtime() or time.gmtime(). To change it for all
115 | formatters, for example if you want all logging times to be shown in GMT,
116 | set the 'converter' attribute in the Formatter class.
117 | """
118 | if datefmt is None:
119 | datefmt = self.datefmt
120 | ct = self.converter(record.created)
121 | if datefmt:
122 | s = time.strftime(datefmt, ct)
123 | else:
124 | s = time.strftime(self.default_time_format, ct)
125 | if self.default_msec_format:
126 | s = self.default_msec_format % (s, record.msecs)
127 | return s
128 |
129 | def formatException(self, ei):
130 | """
131 | Format and return the specified exception information as a string.
132 |
133 | This default implementation just uses
134 | traceback.print_exception()
135 | """
136 | sio = io.StringIO()
137 | tb = ei[2]
138 | # See issues #9427, #1553375. Commented out for now.
139 | # if getattr(self, 'fullstack', False):
140 | # traceback.print_stack(tb.tb_frame.f_back, file=sio)
141 | traceback.print_exception(ei[0], ei[1], tb, None, sio)
142 | s = sio.getvalue()
143 | sio.close()
144 | if s[-1:] == "\n":
145 | s = s[:-1]
146 | return s
147 |
148 | def usesTime(self):
149 | """
150 | Check if the format uses the creation time of the record.
151 | """
152 | return True
153 |
154 | def formatMessage(self, record):
155 | return self._style.format(record)
156 |
157 | def formatStack(self, stack_info):
158 | """
159 | This method is provided as an extension point for specialized
160 | formatting of stack information.
161 |
162 | The input data is a string as returned from a call to
163 | :func:`traceback.print_stack`, but with the last trailing newline
164 | removed.
165 |
166 | The base implementation just returns the value passed in.
167 | """
168 | return stack_info
169 |
170 | def format(self, record):
171 | """
172 | Format the specified record as text.
173 |
174 | The record's attribute dictionary is used as the operand to a
175 | string formatting operation which yields the returned string.
176 | Before formatting the dictionary, a couple of preparatory steps
177 | are carried out. The message attribute of the record is computed
178 | using LogRecord.getMessage(). If the formatting string uses the
179 | time (as determined by a call to usesTime(), formatTime() is
180 | called to format the event time. If there is exception information,
181 | it is formatted using formatException() and appended to the message.
182 | """
183 | record.message = record.getMessage()
184 | if self.usesTime():
185 | record.asctime = self.formatTime(record, self.datefmt)
186 | s = self.formatMessage(record)
187 | if record.exc_info and not record.exc_text:
188 | record.exc_text = self.formatException(record.exc_info)
189 | if record.exc_text:
190 | if s[-1:] != "\n":
191 | s = s + "\n"
192 | s = s + record.exc_text
193 | if record.stack_info:
194 | if s[-1:] != "\n":
195 | s = s + "\n"
196 | s = s + self.formatStack(record.stack_info)
197 | return s
198 |
199 |
200 | class ConsoleHandler(StreamHandler):
201 | """
202 | A handler class which writes logging records, appropriately formatted,
203 | to a stream. Note that this class does not close the stream, as
204 | sys.stdout or sys.stderr may be used.
205 | """
206 |
207 | terminator: str
208 | stream: io.IOBase
209 | formatter: ConsoleFormatter
210 |
211 | def __init__(self):
212 | """
213 | Initialize the handler.
214 | """
215 | self.level = logging.root.level
216 | formatter = ConsoleFormatter()
217 | self.formatter = formatter
218 | self.fmt = formatter
219 | super(ConsoleHandler, self).__init__(stream=sys.stderr)
220 | super(StreamHandler, self).__init__(level=logging.DEBUG)
221 | self.formatter = formatter
222 | self.fmt = formatter
223 |
224 | def flush(self):
225 | """
226 | Flushes the stream.
227 | """
228 | self.acquire()
229 | try:
230 | if self.stream and hasattr(self.stream, "flush"):
231 | self.stream.flush()
232 | finally:
233 | self.release()
234 |
235 | def format(self, record):
236 | """
237 | Format the specified record.
238 |
239 | If a formatter is set, use it. Otherwise, use the default formatter
240 | for the module.
241 | """
242 | if self.formatter is None:
243 | self.formatter = ConsoleFormatter()
244 | fmt: ConsoleFormatter = self.formatter
245 | return fmt.format(record)
246 |
247 | def emit(self, record):
248 | """
249 | Emit a record.
250 |
251 | If a formatter is specified, it is used to format the record.
252 | The record is then written to the stream with a trailing newline. If
253 | exception information is present, it is formatted using
254 | traceback.print_exception and appended to the stream. If the stream
255 | has an 'encoding' attribute, it is used to determine how to do the
256 | output to the stream.
257 | """
258 | try:
259 | msg = self.format(record)
260 | stream = self.stream
261 | # issue 35046: merged two stream.writes into one.
262 | stream.write(msg + self.terminator)
263 | self.flush()
264 | except (RecursionError, Exception): # See issue 36272
265 | raise
266 |
267 | def setStream(self, stream):
268 | """
269 | Sets the StreamHandler's stream to the specified value,
270 | if it is different.
271 |
272 | Returns the old stream, if the stream was changed, or None
273 | if it wasn't.
274 | """
275 | if stream is self.stream:
276 | result = None
277 | else:
278 | result = self.stream
279 | self.acquire()
280 | try:
281 | self.flush()
282 | self.stream = stream
283 | finally:
284 | self.release()
285 | return result
286 |
287 | def __repr__(self):
288 | return f"<{self.__class__.__name__}>"
289 |
290 |
291 | if use.config.debugging:
292 | logging.root.setLevel(logging.DEBUG)
293 | handler = ConsoleHandler()
294 | logging.root.handlers.append(handler)
295 |
--------------------------------------------------------------------------------
/src/use/messages.py:
--------------------------------------------------------------------------------
1 | """
2 | Collection of the messages directed to the user.
3 | How it works is quite magical - the lambdas prevent the f-strings from being prematuraly evaluated, and are only evaluated once returned.
4 | Fun fact: f-strings are firmly rooted in the AST.
5 | """
6 |
7 | import webbrowser
8 | from collections import defaultdict, namedtuple
9 | from collections.abc import Callable
10 | from enum import Enum
11 | from pathlib import Path
12 | from shutil import copy
13 | from statistics import geometric_mean, median, stdev
14 |
15 | from beartype import beartype
16 | from jinja2 import Environment, FileSystemLoader, select_autoescape
17 |
18 | import use
19 | from use import __version__, config, home
20 | from use.hash_alphabet import hexdigest_as_JACK
21 | from use.pydantics import PyPI_Release, Version
22 |
23 | env = Environment(
24 | loader=FileSystemLoader(Path(__file__).parent / "templates"),
25 | autoescape=select_autoescape(),
26 | )
27 |
28 |
29 | def std(times):
30 | return stdev(times) if len(times) > 1 else 0
31 |
32 |
33 | def _web_tinny_profiler(timings):
34 | copy(
35 | Path(__file__).absolute().parent / r"templates/profiling.css",
36 | home / "profiling.css",
37 | )
38 | DAT = namedtuple("DECORATOR", "qualname name min geom_mean median stdev len total")
39 | timings_ = [
40 | DAT(
41 | getattr(
42 | func,
43 | "__qualname__",
44 | getattr(func, "__module__", "") + getattr(func, "__name__", str(func)),
45 | ),
46 | func.__name__,
47 | min(times),
48 | geometric_mean(times),
49 | median(times),
50 | std(times),
51 | len(times),
52 | sum(times),
53 | )
54 | for func, times in timings.items()
55 | ]
56 | with open(home / "profiling.html", "w", encoding="utf-8") as file:
57 | args = {
58 | "timings": timings_,
59 | }
60 | file.write(env.get_template("profiling.html").render(**args))
61 | webbrowser.open(f"file://{home}/profiling.html")
62 |
63 |
64 | def _web_aspectized(decorators, functions):
65 | copy(
66 | Path(__file__).absolute().parent / r"templates/aspects.css",
67 | home / "aspects.css",
68 | )
69 | DAT = namedtuple("DECORATOR", "name func")
70 | redecorated = []
71 | for ID, funcs in decorators.items():
72 | name = functions[ID][-1].__name__
73 | redecorated.append(DAT(name, funcs))
74 |
75 | with open(home / "aspects.html", "w", encoding="utf-8") as file:
76 | args = {
77 | "decorators": redecorated,
78 | "functions": functions,
79 | }
80 | file.write(env.get_template("aspects.html").render(**args))
81 | webbrowser.open(f"file://{home}/aspects.html")
82 |
83 |
84 | @beartype
85 | def _web_aspectized_dry_run(
86 | *, decorator: Callable, hits: list, check: Callable, pattern: str, mod_name: str
87 | ):
88 | copy(
89 | Path(__file__).absolute().parent / r"templates/aspects.css",
90 | home / "aspects.css",
91 | )
92 | with open(home / "aspects_dry_run.html", "w", encoding="utf-8") as file:
93 | args = {
94 | "decorator": decorator,
95 | "hits": hits,
96 | "check": check,
97 | "mod_name": mod_name,
98 | }
99 | file.write(env.get_template("aspects_dry_run.html").render(**args))
100 | webbrowser.open(f"file://{home}/aspects_dry_run.html")
101 |
102 |
103 | def _web_pebkac_no_version_no_hash(*, name, pkg_name, version, no_browser: bool):
104 | if not no_browser:
105 | webbrowser.open(f"https://snyk.io/advisor/python/{pkg_name}")
106 | return f"""Please specify version and hash for auto-installation of {pkg_name!r}.
107 | {"" if no_browser else "A webbrowser should open to the Snyk Advisor to check whether the package is vulnerable or malicious."}
108 | If you want to auto-install the latest version, try the following line to select all viable hashes:
109 | use("{name}", version="{version!s}", modes=use.auto_install)"""
110 |
111 |
112 | @beartype
113 | def _web_pebkac_no_hash(
114 | *,
115 | name: str,
116 | pkg_name: str,
117 | version: Version,
118 | releases: list[PyPI_Release],
119 | ):
120 | copy(
121 | Path(__file__).absolute().parent / r"templates/stylesheet.css",
122 | home / "stylesheet.css",
123 | )
124 | entry = namedtuple("Entry", "python platform hash_name hash_value jack_value")
125 | table = defaultdict(lambda: [])
126 | for rel in (rel for rel in releases if rel.version == version):
127 | for hash_name, hash_value in rel.digests.items():
128 | if hash_name not in (x.name for x in use.Hash):
129 | continue
130 | table[hash_name].append(
131 | entry(
132 | rel.justuse.python_tag,
133 | rel.justuse.platform_tag,
134 | hash_name,
135 | hash_value,
136 | hexdigest_as_JACK(hash_value),
137 | )
138 | )
139 |
140 | with open(home / "web_exception.html", "w", encoding="utf-8") as file:
141 | args = {
142 | "name": name,
143 | "pkg_name": pkg_name,
144 | "version": version,
145 | "table": table,
146 | }
147 | file.write(env.get_template("hash-presentation.html").render(**args))
148 |
149 | # from base64 import b64encode
150 | # def data_uri_from_html(html_string):
151 | # return f'data:text/html;base64,{b64encode(html_string.encode()).decode()}'
152 | webbrowser.open(f"file://{home}/web_exception.html")
153 |
154 |
155 | class UserMessage(Enum):
156 | not_reloadable = (
157 | lambda name: f"Beware {name} also contains non-function objects, it may not be safe to reload!"
158 | )
159 | couldnt_connect_to_db = (
160 | lambda e: f"Could not connect to the registry database, please make sure it is accessible. ({e})"
161 | )
162 | use_version_warning = (
163 | lambda max_version: f"""Justuse is version {Version(__version__)}, but there is a newer version {max_version} available on PyPI.
164 | To find out more about the changes check out https://github.com/amogorkon/justuse/wiki/What's-new
165 | Please consider upgrading via
166 | python -m pip install -U justuse
167 | """
168 | )
169 | cant_use = (
170 | lambda thing: f"Only pathlib.Path, yarl.URL and str are valid sources of things to import, but got {type(thing)}."
171 | )
172 | web_error = (
173 | lambda url,
174 | response: f"Could not load {url} from the interwebs, got a {response.status_code} error."
175 | )
176 | no_validation = (
177 | lambda url,
178 | hash_algo,
179 | this_hash: f"""Attempting to import from the interwebs with no validation whatsoever!
180 | To safely reproduce:
181 | use(use.URL('{url}'), hash_algo=use.{hash_algo}, hash_value='{this_hash}')"""
182 | )
183 | version_warning = (
184 | lambda pkg_name,
185 | target_version,
186 | this_version: f"{pkg_name} expected to be version {target_version}, but got {this_version} instead"
187 | )
188 | ambiguous_name_warning = (
189 | lambda pkg_name: f"Attempting to load the pkg '{pkg_name}', if you rather want to use the local module: use(use._ensure_path('{pkg_name}.py'))"
190 | )
191 | pebkac_missing_hash = (
192 | lambda *,
193 | name,
194 | pkg_name,
195 | version,
196 | recommended_hash,
197 | no_browser: f"""Failed to auto-install {pkg_name!r} because hashes aren't specified.
198 | {"" if no_browser else "A webbrowser should open with a list of available hashes for different platforms for you to pick."}"
199 | If you want to use the package only on this platform, this should work:
200 | use("{name}", version="{version!s}", hashes={recommended_hash!r}, modes=use.auto_install)"""
201 | )
202 | pebkac_unsupported = (
203 | lambda pkg_name: f"We could not find any version or release for {pkg_name} that could satisfy our requirements!"
204 | )
205 | pip_json_mess = (
206 | lambda pkg_name,
207 | target_version: f"Tried to auto-install {pkg_name} {target_version} but failed because there was a problem with the JSON from PyPI."
208 | )
209 | pebkac_no_version_no_hash = _web_pebkac_no_version_no_hash
210 | cant_import = (
211 | lambda pkg_name: f"No pkg installed named {pkg_name} and auto-installation not requested. Aborting."
212 | )
213 | cant_import_no_version = (
214 | lambda pkg_name: f"Failed to auto-install '{pkg_name}' because no version was specified."
215 | )
216 |
217 | no_distribution_found = (
218 | lambda pkg_name,
219 | version,
220 | last_version: f"Failed to find any distribution for {pkg_name} version {version} that can be run on this platform. (For your information, the most recent version of {pkg_name} is {last_version})"
221 | )
222 |
223 | no_recommendation = (
224 | lambda pkg_name,
225 | version: f"We could not find any release for {pkg_name} {version} that appears to be compatible with this platform. Check your browser for a list of hashes and select manually."
226 | )
227 | bad_version_given = (
228 | lambda pkg_name,
229 | version: f"{pkg_name} apparently has no version {version}, please check your spelling."
230 | )
231 |
232 |
233 | class StrMessage(UserMessage):
234 | cant_import = (
235 | lambda pkg_name: f"No pkg installed named {pkg_name} and auto-installation not requested. Aborting."
236 | )
237 |
238 |
239 | class TupleMessage(UserMessage):
240 | pass
241 |
242 |
243 | class KwargMessage(UserMessage):
244 | pass
245 |
246 |
247 | def _web_aspectizing_overview(*, decorator, check, pattern, visited, hits):
248 | msg = f"""
249 |
250 |
251 |
252 |
253 |
254 |
Auto-installation of {{package_name}} version {{version}}
21 |
{{name}}
22 |
{{version}}
23 | Please specify the hash corresponding to the platform you want to run your code on and the python version you
24 | want to run your code with.
25 | Values from different hash algorithms can't be mixed together, but regular hexdigests and JACK
26 | representations are interchangeable. After you selected everything you want, copy & paste the snippet below into
27 | your code.
28 |
29 |
30 |