├── .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 | 6 | -------------------------------------------------------------------------------- /.idea/justuse.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 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 | [![license](https://img.shields.io/github/license/amogorkon/justuse)](https://github.com/amogorkon/justuse/blob/main/LICENSE) 2 | [![stars](https://img.shields.io/github/stars/amogorkon/justuse?style=plastic)](https://github.com/amogorkon/justuse/stargazers) 3 | [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://GitHub.com/amogorkon/justuse/graphs/commit-activity) 4 | [![Updates](https://pyup.io/repos/github/amogorkon/justuse/shield.svg)](https://pyup.io/repos/github/amogorkon/justuse/) 5 | [![Build](https://github.com/amogorkon/justuse/actions/workflows/blank.yml/badge.svg?branch=main)](https://github.com/amogorkon/justuse/actions/workflows/blank.yml) 6 | [![Downloads](https://pepy.tech/badge/justuse)](https://pepy.tech/project/justuse) 7 | [![justuse](https://snyk.io/advisor/python/justuse/badge.svg)](https://snyk.io/advisor/python/justuse) 8 | [![slack](https://img.shields.io/badge/slack-@justuse-purple.svg?logo=slack)](https://join.slack.com/t/justuse/shared_invite/zt-tot4bhq9-_qIXBdeiRIfhoMjxu0EhFw) 9 | [![codecov](https://codecov.io/gh/amogorkon/justuse/branch/unstable/graph/badge.svg?token=ROM5GP7YGV)](https://codecov.io/gh/amogorkon/justuse) 10 | [![Sourcery](https://img.shields.io/badge/Sourcery-enabled-brightgreen)](https://sourcery.ai) 11 | Code style: black 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 |
    255 | {"".join(f"
  • {h}
  • " for h in hits)} 256 |
257 | 258 | 259 | """ 260 | 261 | with open(use.home / "aspectizing_overview.html", "w") as f: 262 | f.write(msg) 263 | if not config.testing: 264 | webbrowser.open(use.home / "aspectizing_overview.html") 265 | return msg 266 | -------------------------------------------------------------------------------- /src/use/pydantics.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pydantic models for JustUse for validation of PyPI API responses and other data. 3 | """ 4 | 5 | import os 6 | import re 7 | from logging import INFO, getLogger 8 | from pathlib import Path 9 | 10 | import packaging 11 | from packaging.version import Version as PkgVersion 12 | from pydantic import BaseModel 13 | 14 | log = getLogger(__name__) 15 | 16 | 17 | class Configuration(BaseModel): 18 | debug_level: int = INFO # 20 19 | version_warning: bool = True 20 | no_browser: bool = False 21 | disable_jack: bool = bool(int(os.getenv("DISABLE_JACK", "0"))) 22 | debugging: bool = bool(int(os.getenv("DEBUG", "0"))) 23 | use_db: bool = bool(int(os.getenv("USE_DB", "1"))) 24 | testing: bool = bool(int(os.getenv("TESTING", "0"))) 25 | home: Path = Path( 26 | os.getenv("JUSTUSE_HOME", str(Path.home() / ".justuse-python")) 27 | ).absolute() 28 | venv: Path = ( 29 | Path(os.getenv("JUSTUSE_HOME", str(Path.home() / ".justuse-python"))).absolute() 30 | / "venv" 31 | ) 32 | packages: Path = ( 33 | Path(os.getenv("JUSTUSE_HOME", str(Path.home() / ".justuse-python"))).absolute() 34 | / "packages" 35 | ) 36 | web_modules: Path = ( 37 | Path(os.getenv("JUSTUSE-HOME", str(Path.home() / ".justuse-python"))) 38 | / "web-modules" 39 | ) 40 | logs: Path = ( 41 | Path(os.getenv("JUSTUSE_HOME", str(Path.home() / ".justuse-python"))).absolute() 42 | / "logs" 43 | ) 44 | registry: Path = ( 45 | Path(os.getenv("JUSTUSE_HOME", str(Path.home() / ".justuse-python"))).absolute() 46 | / "registry.db" 47 | ) 48 | 49 | class Config: 50 | validate_assignment = True 51 | 52 | 53 | class git(BaseModel): 54 | repo: str 55 | host: str = "github.com" 56 | branch: str = "main" 57 | commit: str | None = None 58 | 59 | 60 | class Version(PkgVersion): 61 | """Well, apparently they refuse to make Version iterable, so we'll have to do it ourselves. 62 | This is necessary to compare sys.version_info with Version and make some tests more elegant, amongst other things.""" 63 | 64 | def __new__(cls, *args, **kwargs): 65 | if args and isinstance(args[0], Version): 66 | return args[0] 67 | else: 68 | return super(cls, Version).__new__(cls) 69 | 70 | def __init__( 71 | self, 72 | versionobj: PkgVersion | str | None = None, 73 | *, 74 | major=0, 75 | minor=0, 76 | patch=0, 77 | ): 78 | if isinstance(versionobj, Version): 79 | return 80 | 81 | if versionobj: 82 | super(Version, self).__init__(versionobj) 83 | return 84 | 85 | if major is None or minor is None or patch is None: 86 | raise ValueError( 87 | f"Either 'Version' must be initialized with either a string, packaging.version.Verson, {__class__.__qualname__}, or else keyword arguments for 'major', 'minor' and 'patch' must be provided. Actual invocation was: {__class__.__qualname__}({versionobj!r}, {major=!r}, {minor=!r})" 88 | ) 89 | 90 | # string as only argument 91 | # no way to construct a Version otherwise - WTF 92 | versionobj = ".".join(map(str, (major, minor, patch))) 93 | super(Version, self).__init__(versionobj) 94 | 95 | def __iter__(self): 96 | yield from self.release 97 | 98 | def __repr__(self): 99 | return f"Version('{super().__str__()}')" 100 | 101 | def __hash__(self): 102 | return hash(self._version) 103 | 104 | @classmethod 105 | def __get_validators__(cls): 106 | yield cls.validate 107 | 108 | @classmethod 109 | def validate(cls, value, info): 110 | return Version(value) 111 | 112 | 113 | def _delete_none(a_dict: dict[str, object]) -> dict[str, object]: 114 | for k, v in tuple(a_dict.items()): 115 | if v is None or v == "": 116 | del a_dict[k] 117 | return a_dict 118 | 119 | 120 | class RegistryEntry(BaseModel): 121 | class Config: 122 | validate_assignment = True 123 | 124 | artifact_path: Path 125 | installation_path: Path 126 | pure_python_package: bool 127 | 128 | 129 | class JustUse_Info(BaseModel): 130 | distribution: str | None = None 131 | version: str | None = None 132 | build_tag: str | None = None 133 | python_tag: str | None = None 134 | abi_tag: str | None = None 135 | platform_tag: str | None = None 136 | ext: str | None = None 137 | 138 | 139 | class PyPI_Release(BaseModel): 140 | abi_tag: str | None = None 141 | build_tag: str | None = None 142 | distribution: str | None = None 143 | digests: dict[str, str] 144 | ext: str | None = None 145 | filename: str 146 | requires_python: str | None = None 147 | packagetype: str 148 | platform_tag: str | None = None 149 | python_version: str | None = None 150 | python_tag: str | None = None 151 | url: str 152 | version: Version 153 | yanked: bool 154 | 155 | class Config: 156 | validate_assignment = True 157 | 158 | @property 159 | def is_sdist(self): 160 | return ( 161 | self.packagetype == "sdist" 162 | or self.python_version == "source" 163 | or self.justuse.abi_tag == "none" 164 | ) 165 | 166 | # TODO: cleanup, this is too weird 167 | @property 168 | def justuse(self) -> JustUse_Info: 169 | pp = Path(self.filename) 170 | if ".tar" in self.filename: 171 | ext = self.filename[self.filename.index(".tar") + 1 :] 172 | else: 173 | ext = pp.name[len(pp.stem) + 1 :] 174 | rest = pp.name[: -len(ext) - 1] 175 | 176 | if match := re.match( 177 | f"{_not_dash('distribution')}-{_not_dash('version')}-?{_not_dash_with_int('build_tag')}?-?{_not_dash('python_tag')}?-?{_not_dash('abi_tag')}?-?{_not_dash('platform_tag')}?", 178 | rest, 179 | ): 180 | return JustUse_Info(**_delete_none(match.groupdict()), ext=ext) 181 | return JustUse_Info() 182 | 183 | 184 | def _not_dash(name: str) -> str: 185 | return f"(?P<{name}>[^-]+)" 186 | 187 | 188 | def _not_dash_with_int(name: str) -> str: 189 | return f"(?P<{name}>[0-9][^-]*)" 190 | 191 | 192 | class PyPI_Downloads(BaseModel): 193 | last_day: int 194 | last_month: int 195 | last_week: int 196 | 197 | 198 | class PyPI_Info(BaseModel): 199 | class Config: 200 | extra = "ignore" 201 | 202 | description_content_type: str | None 203 | download_url: str | None 204 | pkg_name: str | None 205 | package_url: str 206 | platform: str | None 207 | project_url: str | None 208 | project_urls: dict[str, str] | None 209 | release_url: str | None 210 | requires_dist: list[str] | None 211 | requires_python: str | None 212 | summary: str | None 213 | version: str | None 214 | yanked: bool | None 215 | yanked_reason: str | None 216 | 217 | 218 | class PyPI_URL(BaseModel): 219 | abi_tag: str | None 220 | build_tag: str | None 221 | digests: dict[str, str] 222 | url: str 223 | packagetype: str 224 | requires_python: str | None 225 | python_version: str | None 226 | filename: str 227 | yanked: bool 228 | distribution: str | None 229 | python_tag: str | None 230 | platform_tag: str | None 231 | ext: str | None 232 | 233 | 234 | class PyPI_Project(BaseModel): 235 | releases: dict[Version, list[PyPI_Release]] | None = {} 236 | urls: list[PyPI_URL] = None 237 | last_serial: int = None 238 | info: PyPI_Info = None 239 | 240 | class Config: 241 | extra = "ignore" 242 | 243 | def __init__(self, *, releases=None, urls, info, **kwargs): 244 | try: 245 | for version in list(releases.keys()): 246 | if not isinstance(version, str): 247 | continue 248 | try: 249 | Version(version) 250 | except packaging.version.InvalidVersion: 251 | del releases[version] 252 | 253 | def get_info(rel_info, ver_str): 254 | data = { 255 | **rel_info, 256 | **_parse_filename(rel_info["filename"]), 257 | "version": Version(str(ver_str)), 258 | } 259 | if info.get("requires_python"): 260 | data["requires_python"] = info.get("requites_python") 261 | if info.get("requires_dist"): 262 | data["requires_dist"] = info.get("requires_dist") 263 | return data 264 | 265 | super(PyPI_Project, self).__init__( 266 | releases={ 267 | str(ver_str): [ 268 | get_info(rel_info, ver_str) for rel_info in release_infos 269 | ] 270 | for ver_str, release_infos in releases.items() 271 | }, 272 | urls=[ 273 | get_info(rel_info, ver_str) 274 | for ver_str, rel_infos in releases.items() 275 | for rel_info in rel_infos 276 | ], 277 | info=info, 278 | **kwargs, 279 | ) 280 | except AttributeError: 281 | pass 282 | finally: 283 | releases = None 284 | info = None 285 | urls = None 286 | 287 | 288 | def _parse_filename(filename) -> dict: 289 | """ 290 | REFERENCE IMPLEMENTATION - DO NOT USE 291 | Match the filename and return a dict of parts. 292 | >>> parse_filename("numpy-1.19.5-cp36-cp36m-macosx_10_9_x86_64.whl") 293 | {'distribution': 'numpy', 'version': '1.19.5', 'build_tag', 'python_tag': 'cp36', 'abi_tag': 'cp36m', 'platform_tag': 'macosx_10_9_x86_64', 'ext': 'whl'} 294 | """ 295 | # Filename as API, seriously WTF... 296 | assert isinstance(filename, str) 297 | distribution = version = build_tag = python_tag = abi_tag = platform_tag = None 298 | pp = Path(filename) 299 | packagetype = None 300 | if ".tar" in filename: 301 | ext = filename[filename.index(".tar") :] 302 | packagetype = "source" 303 | else: 304 | ext = pp.name[len(pp.stem) + 1 :] 305 | packagetype = "bdist" 306 | rest = pp.name[: -len(ext) - 1] 307 | 308 | p = rest.split("-") 309 | np = len(p) 310 | if np == 2: 311 | distribution, version = p 312 | elif np == 3: 313 | distribution, version, python_tag = p 314 | elif np == 5: 315 | distribution, version, python_tag, abi_tag, platform_tag = p 316 | elif np == 6: 317 | distribution, version, build_tag, python_tag, abi_tag, platform_tag = p 318 | else: 319 | return {} 320 | 321 | return { 322 | "distribution": distribution, 323 | "version": version, 324 | "build_tag": build_tag, 325 | "python_tag": python_tag, 326 | "abi_tag": abi_tag, 327 | "platform_tag": platform_tag, 328 | "ext": ext, 329 | "filename": filename, 330 | "packagetype": packagetype, 331 | "yanked_reason": "", 332 | "bugtrack_url": "", 333 | } 334 | -------------------------------------------------------------------------------- /src/use/templates/aspects.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | body { 4 | font-family: Arial Unicode MS, Lucida Sans Unicode, DejaVu Sans, Quivira, Symbola, Code2000; 5 | padding: 30px; 6 | line-height: 1.6; 7 | background:#131313; 8 | color:#fff; 9 | } 10 | 11 | * { 12 | box-sizing: border-box; 13 | } 14 | 15 | h1 { 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | margin: 0; 20 | animation: glitch 5s linear infinite alternate-reverse; 21 | color: #50BFE6; 22 | } 23 | 24 | @keyframes glitch{ 25 | 2%,64%{ 26 | transform: translate(2px,0) skew(50deg); 27 | opacity: 0; 28 | text-shadow: 0 0 50px #50BFE6; 29 | } 30 | 4%,60%{ 31 | transform: translate(-2px,0) skew(-10deg); 32 | opacity: 0.7; 33 | text-shadow: 0 0 20px #50BFE6; 34 | } 35 | 62%{ 36 | transform: translate(0,0) skew(0deg); 37 | opacity: 1; 38 | text-shadow: 0 0 10px #50BFE6; 39 | } 40 | 80%{ 41 | opacity: 1; 42 | text-shadow: 0 0 5px #50BFE6; 43 | } 44 | } 45 | 46 | h1:before, 47 | h1:after{ 48 | content: attr(title); 49 | position: absolute; 50 | left: 0; 51 | } 52 | 53 | h1:before{ 54 | animation: glitchTop 1s linear infinite; 55 | clip-path: polygon(0 0, 100% 0, 100% 33%, 0 33%); 56 | -webkit-clip-path: polygon(0 0, 100% 0, 100% 33%, 0 33%); 57 | } 58 | 59 | @keyframes glitchTop{ 60 | 2%,64%{ 61 | transform: translate(2px,-2px); 62 | } 63 | 4%,60%{ 64 | transform: translate(-2px,2px); 65 | } 66 | 62%{ 67 | transform: translate(13px,-1px) skew(-13deg); 68 | } 69 | } 70 | 71 | h1:after{ 72 | animation: glitchBottom 1.5s linear infinite; 73 | clip-path: polygon(0 67%, 100% 67%, 100% 100%, 0 100%); 74 | -webkit-clip-path: polygon(0 67%, 100% 67%, 100% 100%, 0 100%); 75 | } 76 | 77 | @keyframes glitchBottom{ 78 | 2%,64%{ 79 | transform: translate(-2px,0); 80 | } 81 | 4%,60%{ 82 | transform: translate(-2px,0); 83 | } 84 | 62%{ 85 | transform: translate(-22px,5px) skew(21deg); 86 | } 87 | } 88 | 89 | section { 90 | position: relative; 91 | margin: 0 auto; 92 | z-index: 0; 93 | color: white; 94 | } 95 | 96 | 97 | 98 | th { 99 | border-bottom:1px solid black;} 100 | 101 | tr {border: 1px white; } 102 | 103 | p { 104 | font-size: 1em; 105 | } 106 | 107 | div { 108 | display: inline-block; 109 | } 110 | 111 | div:focus { 112 | outline: none; 113 | } 114 | 115 | body { 116 | padding: 3em; 117 | } 118 | 119 | .p { 120 | margin-top: 300px; 121 | text-align: center; 122 | display: block; 123 | font-size: 1em; 124 | } 125 | 126 | .filter_field > button { 127 | vertical-align: middle; 128 | } 129 | .filter_field > textarea { 130 | vertical-align: middle; 131 | } 132 | 133 | table { 134 | left: 0; 135 | top: 38px; 136 | width: 100%; 137 | font-size: 1em; 138 | text-align: left; 139 | background: rgb(1, 18, 114); 140 | padding: 40px 0; 141 | border-collapse: collapse; 142 | color: white; 143 | font-weight: lighter; 144 | } 145 | 146 | .selected { 147 | padding: 5px; 148 | border-bottom: gray solid 2px; 149 | border-top: white solid 2px; 150 | background: #b5bfcf; 151 | box-shadow: inset 1px 1px 3px #000; 152 | color: rgb(51, 39, 39); 153 | font-weight: bold; 154 | 155 | } 156 | .failure { 157 | background: red; 158 | } 159 | 160 | .visible { 161 | visibility: visible; 162 | opacity: 1; 163 | transition: opacity 2s linear; 164 | } 165 | 166 | .hidden { 167 | visibility: hidden; 168 | opacity: 0; 169 | transition: visibility 0s 2s, opacity 2s linear; 170 | } -------------------------------------------------------------------------------- /src/use/templates/aspects.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | Justuse 16 | 17 | 18 | 19 |
20 |

Applied Aspects

21 |
22 |

Here is a list of objects we have decorated, in order of application.

23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | {% for entry in decorators %} 31 | 32 | 33 | 34 | 35 | {% endfor %} 36 |
name decorators
{{ entry.name }} {{ entry.func}}
37 |
38 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/use/templates/aspects_dry_run.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | Justuse 16 | 17 | 18 | 19 |
20 | 21 | 22 |

Dry Run for applying {{decorator.__name__}} to <{{mod_name}}> 23 |

24 |
25 |

Here are all the objects we could decorate inside module/package <{{mod_name}}>.

26 |

27 | 28 |

29 |

30 | 31 |

32 | 33 |

Options for filtering

34 | 35 |

36 | 37 | 41 |

42 | 43 | check this out if you need help building the pattern 44 |

45 | 46 |

47 | 48 | 49 |

50 | 51 |

52 | 53 |

57 |

58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | {% for entry in hits %} 68 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | {% endfor %} 77 |
qualname name type
78 | 79 | 80 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /src/use/templates/hash-presentation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 13 | 14 | 15 | Justuse 16 | 17 | 18 | 19 |
20 |

Auto-installation of {{package_name}} version {{version}}

21 | 22 | 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 |

31 | 32 |

33 |

34 | 37 |

38 |

39 | 43 | 44 | 45 |

46 | {% for hash_name in table.keys()%} 47 |
48 |
49 | 50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {% for entry in table[hash_name] %} 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | {% endfor %} 67 |
Python Platform Hash
{{ entry.python }} {{ entry.platform }} {{ entry.hash_value}}
68 |
69 | {% endfor %} 70 | 71 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /src/use/templates/profiling.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | body { 4 | font-family: Arial Unicode MS, Lucida Sans Unicode, DejaVu Sans, Quivira, Symbola, Code2000; 5 | padding: 30px; 6 | line-height: 1.6; 7 | background:#131313; 8 | color:#fff; 9 | } 10 | 11 | * { 12 | box-sizing: border-box; 13 | } 14 | 15 | h1 { 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | margin: 0; 20 | animation: glitch 5s linear infinite alternate-reverse; 21 | color: #50BFE6; 22 | } 23 | 24 | @keyframes glitch{ 25 | 2%,64%{ 26 | transform: translate(2px,0) skew(50deg); 27 | opacity: 0; 28 | text-shadow: 0 0 50px #50BFE6; 29 | } 30 | 4%,60%{ 31 | transform: translate(-2px,0) skew(-10deg); 32 | opacity: 0.7; 33 | text-shadow: 0 0 20px #50BFE6; 34 | } 35 | 62%{ 36 | transform: translate(0,0) skew(0deg); 37 | opacity: 1; 38 | text-shadow: 0 0 10px #50BFE6; 39 | } 40 | 80%{ 41 | opacity: 1; 42 | text-shadow: 0 0 5px #50BFE6; 43 | } 44 | } 45 | 46 | h1:before, 47 | h1:after{ 48 | content: attr(title); 49 | position: absolute; 50 | left: 0; 51 | } 52 | 53 | h1:before{ 54 | animation: glitchTop 1s linear infinite; 55 | clip-path: polygon(0 0, 100% 0, 100% 33%, 0 33%); 56 | -webkit-clip-path: polygon(0 0, 100% 0, 100% 33%, 0 33%); 57 | } 58 | 59 | @keyframes glitchTop{ 60 | 2%,64%{ 61 | transform: translate(2px,-2px); 62 | } 63 | 4%,60%{ 64 | transform: translate(-2px,2px); 65 | } 66 | 62%{ 67 | transform: translate(13px,-1px) skew(-13deg); 68 | } 69 | } 70 | 71 | h1:after{ 72 | animation: glitchBottom 1.5s linear infinite; 73 | clip-path: polygon(0 67%, 100% 67%, 100% 100%, 0 100%); 74 | -webkit-clip-path: polygon(0 67%, 100% 67%, 100% 100%, 0 100%); 75 | } 76 | 77 | @keyframes glitchBottom{ 78 | 2%,64%{ 79 | transform: translate(-2px,0); 80 | } 81 | 4%,60%{ 82 | transform: translate(-2px,0); 83 | } 84 | 62%{ 85 | transform: translate(-22px,5px) skew(21deg); 86 | } 87 | } 88 | 89 | section { 90 | position: relative; 91 | margin: 0 auto; 92 | z-index: 0; 93 | color: white; 94 | } 95 | 96 | 97 | 98 | th { 99 | border-bottom:1px solid black;} 100 | 101 | tr {border: 1px white; } 102 | 103 | p { 104 | font-size: 1em; 105 | } 106 | 107 | div { 108 | display: inline-block; 109 | } 110 | 111 | 112 | body { 113 | padding: 3em; 114 | } 115 | 116 | .p { 117 | margin-top: 300px; 118 | text-align: center; 119 | display: block; 120 | font-size: 1em; 121 | } 122 | 123 | 124 | table { 125 | color: black; 126 | left: 0; 127 | top: 38px; 128 | width: 100%; 129 | font-size: 1em; 130 | text-align: left; 131 | background-color: #93beff; 132 | padding: 40px 0; 133 | border-collapse: collapse; 134 | } -------------------------------------------------------------------------------- /src/use/templates/profiling.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 13 | 14 | 15 | Justuse 16 | 17 | 18 | 19 |
20 |

21 |

Tinny Profiler Statistics

22 |

23 |
24 |

These are the statistics of the tinny profiler gathered since applying the decorator.

25 | 26 |

27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {% for entry in timings %} 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {% endfor %} 50 |
QualName Name Number of calls Total time spent Minimum Geometric Mean Median Standard Deviation
{{entry.qualname}}{{entry.name}}{{entry.len}}{{ (entry.total / 10**9)|round(5) }} sec{{ entry.min }} ns ({{ (entry.min / 10**9)|round(5)}} sec){{ entry.geom_mean|int }} ns ({{ (entry.geom_mean / 10**9)|round(5) }} sec){{ entry.median }} ns ({{ (entry.median/ 10**9)|round(5) }} sec){{ entry.stdev|int}} ns ({{(entry.stdev / 10**9)|round(5)}} sec)
51 |

52 | 53 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/use/templates/stylesheet.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | body { 4 | font-family: Arial Unicode MS, Lucida Sans Unicode, DejaVu Sans, Quivira, Symbola, Code2000; 5 | padding: 30px; 6 | line-height: 1.6; 7 | background:#131313; 8 | color:#fff; 9 | } 10 | 11 | * { 12 | box-sizing: border-box; 13 | } 14 | 15 | h1 { 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | margin: 0; 20 | animation: glitch 5s linear infinite alternate-reverse; 21 | color: #50BFE6; 22 | } 23 | 24 | @keyframes glitch{ 25 | 2%,64%{ 26 | transform: translate(2px,0) skew(50deg); 27 | opacity: 0; 28 | text-shadow: 0 0 50px #50BFE6; 29 | } 30 | 4%,60%{ 31 | transform: translate(-2px,0) skew(-10deg); 32 | opacity: 0.7; 33 | text-shadow: 0 0 20px #50BFE6; 34 | } 35 | 62%{ 36 | transform: translate(0,0) skew(0deg); 37 | opacity: 1; 38 | text-shadow: 0 0 10px #50BFE6; 39 | } 40 | 80%{ 41 | opacity: 1; 42 | text-shadow: 0 0 5px #50BFE6; 43 | } 44 | } 45 | 46 | h1:before, 47 | h1:after{ 48 | content: attr(title); 49 | position: absolute; 50 | left: 0; 51 | } 52 | 53 | h1:before{ 54 | animation: glitchTop 1s linear infinite; 55 | clip-path: polygon(0 0, 100% 0, 100% 33%, 0 33%); 56 | -webkit-clip-path: polygon(0 0, 100% 0, 100% 33%, 0 33%); 57 | } 58 | 59 | @keyframes glitchTop{ 60 | 2%,64%{ 61 | transform: translate(2px,-2px); 62 | } 63 | 4%,60%{ 64 | transform: translate(-2px,2px); 65 | } 66 | 62%{ 67 | transform: translate(13px,-1px) skew(-13deg); 68 | } 69 | } 70 | 71 | h1:after{ 72 | animation: glitchBottom 1.5s linear infinite; 73 | clip-path: polygon(0 67%, 100% 67%, 100% 100%, 0 100%); 74 | -webkit-clip-path: polygon(0 67%, 100% 67%, 100% 100%, 0 100%); 75 | } 76 | 77 | @keyframes glitchBottom{ 78 | 2%,64%{ 79 | transform: translate(-2px,0); 80 | } 81 | 4%,60%{ 82 | transform: translate(-2px,0); 83 | } 84 | 62%{ 85 | transform: translate(-22px,5px) skew(21deg); 86 | } 87 | } 88 | 89 | section { 90 | position: relative; 91 | margin: 0 auto; 92 | z-index: 0; 93 | color: white; 94 | } 95 | 96 | table { 97 | color: black; 98 | position: absolute; 99 | left: 0; 100 | top: 38px; 101 | width: 100%; 102 | font-size: 1em; 103 | text-align: left; 104 | background-color: #93beff; 105 | padding: 40px 0; 106 | border-collapse: collapse; 107 | } 108 | 109 | th { 110 | border-bottom:1px solid black;} 111 | 112 | tr {border: 1px white; } 113 | 114 | button { 115 | cursor: pointer; 116 | width: 199px; 117 | display: inline-block; 118 | background-color: #5E6B7F; 119 | color: white; 120 | text-align: center; 121 | transition: .25s ease; 122 | border: none; 123 | padding: 10px; 124 | border-radius: 12px 12px 0 0; 125 | } 126 | 127 | p { 128 | font-size: 1em; 129 | } 130 | 131 | div { 132 | display: inline-block; 133 | } 134 | 135 | div:focus { 136 | outline: none; 137 | } 138 | 139 | body { 140 | padding: 3em; 141 | } 142 | 143 | .p { 144 | margin-top: 300px; 145 | text-align: center; 146 | display: block; 147 | font-size: 1em; 148 | } 149 | 150 | .button { 151 | cursor: pointer; 152 | width: 199px; 153 | display: inline-block; 154 | text-align: center; 155 | transition: .25s ease; 156 | border-style: outset; 157 | border-radius: 12px 12px 12px 12px; 158 | padding: 10px; 159 | color: white; 160 | box-shadow: 0 5px #666; 161 | } 162 | 163 | #clipboard_button { 164 | background-color: #5c0847; 165 | } 166 | 167 | #clipboard_button:active { 168 | box-shadow: none; 169 | transform: translateY(4px); 170 | } 171 | 172 | #jack_button { 173 | appearance: none; 174 | } 175 | 176 | #jack_button_label { 177 | background: #117d86; 178 | } 179 | 180 | #jack_button_label:active { 181 | transform: translateY(4px); 182 | } 183 | 184 | :checked + span { 185 | font-weight: bold; 186 | box-shadow: none; 187 | background: #010102; 188 | } 189 | 190 | .row_selected { 191 | background: #ff79ff; 192 | } -------------------------------------------------------------------------------- /src/use/test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | import shutil 4 | import sys 5 | 6 | from colorama import Fore 7 | 8 | 9 | def get_sources_and_destination() -> tuple[list[str], str]: 10 | """Get the sources and destination from :code:`sys.argv`""" 11 | return sys.argv[1:-1], sys.argv[-1] 12 | 13 | 14 | def log_move(source_path: str, source_to_destination_path: str) -> None: 15 | """Log when files are moved.""" 16 | print(f"{Fore.YELLOW}{source_path} {Fore.RESET}→ {Fore.GREEN}{source_to_destination_path}{Fore.RESET}") 17 | 18 | 19 | def log_not_found(path: str) -> None: 20 | """Log when a path/file doesn't exist.""" 21 | print(f"{Fore.RED}Could not find {Fore.YELLOW}{path}{Fore.RESET}.") 22 | 23 | 24 | def main() -> None: 25 | if len(sys.argv) < 3: 26 | return 27 | 28 | sources, destination = get_sources_and_destination() 29 | 30 | for source in sources: 31 | if not os.path.exists(source): 32 | log_not_found(source) 33 | continue 34 | 35 | log_move(str(pathlib.Path(source)), shutil.move(source, destination)) 36 | 37 | 38 | if __name__ == "__main__": 39 | main() 40 | -------------------------------------------------------------------------------- /src/use/tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module to hold the decorators and other utility functions used in justuse. 3 | """ 4 | 5 | import ast 6 | import inspect 7 | from itertools import takewhile 8 | from textwrap import dedent 9 | 10 | 11 | class _PipeTransformer(ast.NodeTransformer): 12 | def visit_BinOp(self, node): 13 | if not isinstance(node.op, (ast.LShift, ast.RShift)): 14 | return node 15 | if not isinstance(node.right, ast.Call): 16 | return self.visit( 17 | ast.Call( 18 | func=node.right, 19 | args=[node.left], 20 | keywords=[], 21 | starargs=None, 22 | kwargs=None, 23 | lineno=node.right.lineno, 24 | col_offset=node.right.col_offset, 25 | ) 26 | ) 27 | node.right.args.insert( 28 | 0 if isinstance(node.op, ast.RShift) else len(node.right.args), node.left 29 | ) 30 | return self.visit(node.right) 31 | 32 | 33 | def pipes(func_or_class): 34 | if inspect.isclass(func_or_class): 35 | decorator_frame = inspect.stack()[1] 36 | ctx = decorator_frame[0].f_locals 37 | first_line_number = decorator_frame[2] 38 | else: 39 | ctx = func_or_class.__globals__ 40 | first_line_number = func_or_class.__code__.co_firstlineno 41 | source = inspect.getsource(func_or_class) 42 | tree = ast.parse(dedent(source)) 43 | ast.increment_lineno(tree, first_line_number - 1) 44 | source_indent = sum(1 for _ in takewhile(str.isspace, source)) + 1 45 | for node in ast.walk(tree): 46 | if hasattr(node, "col_offset"): 47 | node.col_offset += source_indent 48 | tree.body[0].decorator_list = [ 49 | d 50 | for d in tree.body[0].decorator_list 51 | if isinstance(d, ast.Call) 52 | and d.func.id != "pipes" 53 | or isinstance(d, ast.Name) 54 | and d.id != "pipes" 55 | ] 56 | tree = _PipeTransformer().visit(tree) 57 | code = compile( 58 | tree, filename=(ctx["__file__"] if "__file__" in ctx else "repl"), mode="exec" 59 | ) 60 | exec(code, ctx) 61 | return ctx[tree.body[0].name] 62 | -------------------------------------------------------------------------------- /tests/.tests/.file_for_test387.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amogorkon/justuse/3a478fee76f728f681977ee8bf0738747317bc7f/tests/.tests/.file_for_test387.py -------------------------------------------------------------------------------- /tests/.tests/.test0.py: -------------------------------------------------------------------------------- 1 | import use 2 | 3 | use(use.Path(".test1.py")) 4 | 5 | # mod1 = use("numpy") 6 | # print(mod1) 7 | # print(mod1.__version__) 8 | 9 | #mod2 = use("numpy", version="1.19.2", modes=use.auto_install) 10 | 11 | #print(mod2) 12 | #print(mod2.__version__) 13 | print("end") 14 | -------------------------------------------------------------------------------- /tests/.tests/.test1.py: -------------------------------------------------------------------------------- 1 | from random import choice, seed 2 | 3 | seed(15^3) 4 | 5 | print(choice(["batman", "spiderman", "panther"])) -------------------------------------------------------------------------------- /tests/.tests/.test2.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | import contextlib 3 | 4 | with contextlib.suppress(importlib.metadata.PackageNotFoundError): 5 | installed_version = None 6 | installed_version = importlib.metadata.version("foobar") -------------------------------------------------------------------------------- /tests/.tests/.test3.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amogorkon/justuse/3a478fee76f728f681977ee8bf0738747317bc7f/tests/.tests/.test3.py -------------------------------------------------------------------------------- /tests/.tests/bar.py: -------------------------------------------------------------------------------- 1 | __reloadable__ = True 2 | 3 | 4 | def test(): 5 | """interesting!""" 6 | return 6 7 | -------------------------------------------------------------------------------- /tests/.tests/discord_enum.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2015-2019 Rapptz 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a 9 | copy of this software and associated documentation files (the "Software"), 10 | to deal in the Software without restriction, including without limitation 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 12 | and/or sell copies of the Software, and to permit persons to whom the 13 | Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | DEALINGS IN THE SOFTWARE. 25 | """ 26 | 27 | import types 28 | import inspect 29 | from collections import namedtuple 30 | from operator import attrgetter 31 | 32 | __all__ = ( 33 | 'Enum', 34 | 'ChannelType', 35 | 'MessageType', 36 | 'VoiceRegion', 37 | 'SpeakingState', 38 | 'VerificationLevel', 39 | 'ContentFilter', 40 | 'Status', 41 | 'DefaultAvatar', 42 | 'RelationshipType', 43 | 'AuditLogAction', 44 | 'AuditLogActionCategory', 45 | 'UserFlags', 46 | 'ActivityType', 47 | 'HypeSquadHouse', 48 | 'NotificationLevel', 49 | 'PremiumType', 50 | 'UserContentFilter', 51 | 'FriendFlags', 52 | 'Theme', 53 | ) 54 | 55 | def _create_value_cls(name): 56 | cls = namedtuple(f'_EnumValue_{name}', 'name value') 57 | cls.__repr__ = lambda self: '<%s.%s: %r>' % (name, self.name, self.value) 58 | cls.__str__ = lambda self: f'{name}.{self.name}' 59 | return cls 60 | 61 | class EnumMeta(type): 62 | def __new__(cls, name, bases, attrs): 63 | value_mapping = {} 64 | member_mapping = {} 65 | member_names = [] 66 | 67 | value_cls = _create_value_cls(name) 68 | for key, value in list(attrs.items()): 69 | is_function = isinstance(value, types.FunctionType) 70 | if key[0] == '_' and not is_function: 71 | continue 72 | 73 | if is_function: 74 | setattr(value_cls, key, value) 75 | del attrs[key] 76 | continue 77 | 78 | if inspect.ismethoddescriptor(value): 79 | continue 80 | 81 | try: 82 | new_value = value_mapping[value] 83 | except KeyError: 84 | new_value = value_cls(name=key, value=value) 85 | value_mapping[value] = new_value 86 | member_names.append(key) 87 | 88 | member_mapping[key] = new_value 89 | attrs[key] = new_value 90 | 91 | attrs['_enum_value_map_'] = value_mapping 92 | attrs['_enum_member_map_'] = member_mapping 93 | attrs['_enum_member_names_'] = member_names 94 | actual_cls = super().__new__(cls, name, bases, attrs) 95 | value_cls._actual_enum_cls_ = actual_cls 96 | return actual_cls 97 | 98 | def __iter__(cls): 99 | return (cls._enum_member_map_[name] for name in cls._enum_member_names_) 100 | 101 | def __repr__(cls): 102 | return '' % cls.__name__ 103 | 104 | @property 105 | def __members__(cls): 106 | return types.MappingProxyType(cls._enum_member_map_) 107 | 108 | def __call__(cls, value): 109 | try: 110 | return cls._enum_value_map_[value] 111 | except (KeyError, TypeError): 112 | raise ValueError("%r is not a valid %s" % (value, cls.__name__)) 113 | 114 | def __getitem__(cls, key): 115 | return cls._enum_member_map_[key] 116 | 117 | def __setattr__(cls, name, value): 118 | raise TypeError('Enums are immutable.') 119 | 120 | def __instancecheck__(self, instance): 121 | # isinstance(x, Y) 122 | # -> __instancecheck__(Y, x) 123 | try: 124 | return instance._actual_enum_cls_ is self 125 | except AttributeError: 126 | return False 127 | 128 | class Enum(metaclass=EnumMeta): 129 | @classmethod 130 | def try_value(cls, value): 131 | try: 132 | return cls._enum_value_map_[value] 133 | except (KeyError, TypeError): 134 | return value 135 | 136 | 137 | class ChannelType(Enum): 138 | text = 0 139 | private = 1 140 | voice = 2 141 | group = 3 142 | category = 4 143 | news = 5 144 | store = 6 145 | 146 | def __str__(self): 147 | return self.name 148 | 149 | class MessageType(Enum): 150 | default = 0 151 | recipient_add = 1 152 | recipient_remove = 2 153 | call = 3 154 | channel_name_change = 4 155 | channel_icon_change = 5 156 | pins_add = 6 157 | new_member = 7 158 | premium_guild_subscription = 8 159 | premium_guild_tier_1 = 9 160 | premium_guild_tier_2 = 10 161 | premium_guild_tier_3 = 11 162 | 163 | class VoiceRegion(Enum): 164 | us_west = 'us-west' 165 | us_east = 'us-east' 166 | us_south = 'us-south' 167 | us_central = 'us-central' 168 | eu_west = 'eu-west' 169 | eu_central = 'eu-central' 170 | singapore = 'singapore' 171 | london = 'london' 172 | sydney = 'sydney' 173 | amsterdam = 'amsterdam' 174 | frankfurt = 'frankfurt' 175 | brazil = 'brazil' 176 | hongkong = 'hongkong' 177 | russia = 'russia' 178 | japan = 'japan' 179 | southafrica = 'southafrica' 180 | india = 'india' 181 | vip_us_east = 'vip-us-east' 182 | vip_us_west = 'vip-us-west' 183 | vip_amsterdam = 'vip-amsterdam' 184 | 185 | def __str__(self): 186 | return self.value 187 | 188 | class SpeakingState(Enum): 189 | none = 0 190 | voice = 1 191 | soundshare = 2 192 | priority = 4 193 | 194 | def __str__(self): 195 | return self.name 196 | 197 | def __int__(self): 198 | return self.value 199 | 200 | class VerificationLevel(Enum): 201 | none = 0 202 | low = 1 203 | medium = 2 204 | high = 3 205 | table_flip = 3 206 | extreme = 4 207 | double_table_flip = 4 208 | 209 | def __str__(self): 210 | return self.name 211 | 212 | class ContentFilter(Enum): 213 | disabled = 0 214 | no_role = 1 215 | all_members = 2 216 | 217 | def __str__(self): 218 | return self.name 219 | 220 | class UserContentFilter(Enum): 221 | disabled = 0 222 | friends = 1 223 | all_messages = 2 224 | 225 | class FriendFlags(Enum): 226 | noone = 0 227 | mutual_guilds = 1 228 | mutual_friends = 2 229 | guild_and_friends = 3 230 | everyone = 4 231 | 232 | class Theme(Enum): 233 | light = 'light' 234 | dark = 'dark' 235 | 236 | class Status(Enum): 237 | online = 'online' 238 | offline = 'offline' 239 | idle = 'idle' 240 | dnd = 'dnd' 241 | do_not_disturb = 'dnd' 242 | invisible = 'invisible' 243 | 244 | def __str__(self): 245 | return self.value 246 | 247 | class DefaultAvatar(Enum): 248 | blurple = 0 249 | grey = 1 250 | gray = 1 251 | green = 2 252 | orange = 3 253 | red = 4 254 | 255 | def __str__(self): 256 | return self.name 257 | 258 | class RelationshipType(Enum): 259 | friend = 1 260 | blocked = 2 261 | incoming_request = 3 262 | outgoing_request = 4 263 | 264 | class NotificationLevel(Enum): 265 | all_messages = 0 266 | only_mentions = 1 267 | 268 | class AuditLogActionCategory(Enum): 269 | create = 1 270 | delete = 2 271 | update = 3 272 | 273 | class AuditLogAction(Enum): 274 | guild_update = 1 275 | channel_create = 10 276 | channel_update = 11 277 | channel_delete = 12 278 | overwrite_create = 13 279 | overwrite_update = 14 280 | overwrite_delete = 15 281 | kick = 20 282 | member_prune = 21 283 | ban = 22 284 | unban = 23 285 | member_update = 24 286 | member_role_update = 25 287 | role_create = 30 288 | role_update = 31 289 | role_delete = 32 290 | invite_create = 40 291 | invite_update = 41 292 | invite_delete = 42 293 | webhook_create = 50 294 | webhook_update = 51 295 | webhook_delete = 52 296 | emoji_create = 60 297 | emoji_update = 61 298 | emoji_delete = 62 299 | message_delete = 72 300 | 301 | @property 302 | def category(self): 303 | lookup = { 304 | AuditLogAction.guild_update: AuditLogActionCategory.update, 305 | AuditLogAction.channel_create: AuditLogActionCategory.create, 306 | AuditLogAction.channel_update: AuditLogActionCategory.update, 307 | AuditLogAction.channel_delete: AuditLogActionCategory.delete, 308 | AuditLogAction.overwrite_create: AuditLogActionCategory.create, 309 | AuditLogAction.overwrite_update: AuditLogActionCategory.update, 310 | AuditLogAction.overwrite_delete: AuditLogActionCategory.delete, 311 | AuditLogAction.kick: None, 312 | AuditLogAction.member_prune: None, 313 | AuditLogAction.ban: None, 314 | AuditLogAction.unban: None, 315 | AuditLogAction.member_update: AuditLogActionCategory.update, 316 | AuditLogAction.member_role_update: AuditLogActionCategory.update, 317 | AuditLogAction.role_create: AuditLogActionCategory.create, 318 | AuditLogAction.role_update: AuditLogActionCategory.update, 319 | AuditLogAction.role_delete: AuditLogActionCategory.delete, 320 | AuditLogAction.invite_create: AuditLogActionCategory.create, 321 | AuditLogAction.invite_update: AuditLogActionCategory.update, 322 | AuditLogAction.invite_delete: AuditLogActionCategory.delete, 323 | AuditLogAction.webhook_create: AuditLogActionCategory.create, 324 | AuditLogAction.webhook_update: AuditLogActionCategory.update, 325 | AuditLogAction.webhook_delete: AuditLogActionCategory.delete, 326 | AuditLogAction.emoji_create: AuditLogActionCategory.create, 327 | AuditLogAction.emoji_update: AuditLogActionCategory.update, 328 | AuditLogAction.emoji_delete: AuditLogActionCategory.delete, 329 | AuditLogAction.message_delete: AuditLogActionCategory.delete, 330 | } 331 | return lookup[self] 332 | 333 | @property 334 | def target_type(self): 335 | v = self.value 336 | if v == -1: 337 | return 'all' 338 | elif v < 10: 339 | return 'guild' 340 | elif v < 20: 341 | return 'channel' 342 | elif v < 30: 343 | return 'user' 344 | elif v < 40: 345 | return 'role' 346 | elif v < 50: 347 | return 'invite' 348 | elif v < 60: 349 | return 'webhook' 350 | elif v < 70: 351 | return 'emoji' 352 | elif v < 80: 353 | return 'message' 354 | 355 | class UserFlags(Enum): 356 | staff = 1 357 | partner = 2 358 | hypesquad = 4 359 | bug_hunter = 8 360 | hypesquad_bravery = 64 361 | hypesquad_brilliance = 128 362 | hypesquad_balance = 256 363 | early_supporter = 512 364 | 365 | class ActivityType(Enum): 366 | unknown = -1 367 | playing = 0 368 | streaming = 1 369 | listening = 2 370 | watching = 3 371 | 372 | class HypeSquadHouse(Enum): 373 | bravery = 1 374 | brilliance = 2 375 | balance = 3 376 | 377 | class PremiumType(Enum): 378 | nitro_classic = 1 379 | nitro = 2 380 | 381 | def try_enum(cls, val): 382 | """A function that tries to turn the value into enum ``cls``. 383 | 384 | If it fails it returns the value instead. 385 | """ 386 | 387 | try: 388 | return cls._enum_value_map_[val] 389 | except (KeyError, TypeError, AttributeError): 390 | return val -------------------------------------------------------------------------------- /tests/.tests/foo.py: -------------------------------------------------------------------------------- 1 | a: int 2 | 3 | 4 | def test(): 5 | print(f"foo.test() returning a: {a}") 6 | return a 7 | -------------------------------------------------------------------------------- /tests/.tests/modA.py: -------------------------------------------------------------------------------- 1 | import use 2 | 3 | print("FROM", __file__) 4 | 5 | use(use.Path("modB.py")) 6 | 7 | 8 | def foo(x): 9 | return x * 2 10 | 11 | 12 | use(use.Path("modA_test.py"), initial_globals=globals()) 13 | 14 | -------------------------------------------------------------------------------- /tests/.tests/modA_test.py: -------------------------------------------------------------------------------- 1 | print("test_modA") 2 | 3 | foo: callable 4 | 5 | 6 | def test_foo(): 7 | assert foo(2) == 4 8 | 9 | 10 | test_foo() 11 | 12 | -------------------------------------------------------------------------------- /tests/.tests/modB.py: -------------------------------------------------------------------------------- 1 | import use 2 | 3 | print("FROM", __file__) 4 | use(use.Path("tests_subdir/modC.py")) 5 | -------------------------------------------------------------------------------- /tests/.tests/modD.py: -------------------------------------------------------------------------------- 1 | # pylance 2 | print("FROM", __file__) 3 | 4 | import os 5 | import sys 6 | 7 | print(sys.version) 8 | 9 | print("modD CWD:", os.getcwd()) 10 | 11 | import modE 12 | 13 | -------------------------------------------------------------------------------- /tests/.tests/modE.py: -------------------------------------------------------------------------------- 1 | print("FROM", __file__) 2 | -------------------------------------------------------------------------------- /tests/.tests/sys.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amogorkon/justuse/3a478fee76f728f681977ee8bf0738747317bc7f/tests/.tests/sys.py -------------------------------------------------------------------------------- /tests/.tests/tests_subdir/modC.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | print("FROM", __file__) 4 | 5 | 6 | # issue #8 7 | from dataclasses import dataclass 8 | 9 | import use 10 | 11 | 12 | @dataclass(init=False, repr=False) 13 | class Stats: 14 | data_length: int # total length of data 15 | peak_count: int # number of detected peaks 16 | 17 | 18 | use(use.Path("../modD.py")) 19 | -------------------------------------------------------------------------------- /tests/.tests/trained_tagger.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amogorkon/justuse/3a478fee76f728f681977ee8bf0738747317bc7f/tests/.tests/trained_tagger.pkl -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | from pathlib import Path 3 | src = import_base = Path(__file__).parent.parent / "src" 4 | cwd = Path().cwd() 5 | os.chdir(src) 6 | sys.path.insert(0, "") if "" not in sys.path else None 7 | 8 | if sys.version_info < (3, 9) and "use" not in sys.modules: 9 | import gc, types, typing 10 | from typing import _GenericAlias as GenericAlias 11 | from abc import ABCMeta 12 | from collections.abc import Callable 13 | from types import CellType 14 | for t in (list, dict, set, tuple, frozenset, ABCMeta, Callable, CellType): 15 | r = gc.get_referents(t.__dict__)[0] 16 | r.update( 17 | { 18 | "__class_getitem__": classmethod(GenericAlias), 19 | } 20 | ) 21 | 22 | 23 | import use 24 | os.chdir(cwd) 25 | -------------------------------------------------------------------------------- /tests/beast.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import re 4 | import subprocess 5 | import sys 6 | import tempfile 7 | import warnings 8 | from collections.abc import Callable 9 | from contextlib import AbstractContextManager, closing, redirect_stdout 10 | from datetime import datetime 11 | from hashlib import sha256 12 | from importlib.metadata import PackageNotFoundError, distribution 13 | from importlib.util import find_spec 14 | from pathlib import Path 15 | from subprocess import STDOUT, check_output 16 | from textwrap import dedent 17 | from threading import _shutdown_locks 18 | from time import time 19 | from types import ModuleType 20 | from unittest.mock import patch 21 | from warnings import catch_warnings, filterwarnings 22 | 23 | import packaging.tags 24 | import packaging.version 25 | import pytest 26 | import requests 27 | from furl import furl as URL 28 | from hypothesis import assume, example, given 29 | from hypothesis import strategies as st 30 | from pytest import fixture, mark, raises, skip 31 | 32 | src = import_base = Path(__file__).parent.parent / "src" 33 | cwd = Path().cwd() 34 | os.chdir(src) 35 | sys.path.insert(0, "") if "" not in sys.path else None 36 | 37 | if sys.version_info < (3, 9) and "use" not in sys.modules: 38 | import gc 39 | import types 40 | import typing 41 | from abc import ABCMeta 42 | from collections.abc import Callable 43 | from types import CellType 44 | from typing import _GenericAlias as GenericAlias 45 | 46 | for t in (list, dict, set, tuple, frozenset, ABCMeta, Callable, CellType): 47 | r = gc.get_referents(t.__dict__)[0] 48 | r.update( 49 | { 50 | "__class_getitem__": classmethod(GenericAlias), 51 | } 52 | ) 53 | 54 | 55 | import use 56 | 57 | os.chdir(cwd) 58 | 59 | is_win = sys.platform.startswith("win") 60 | 61 | __package__ = "tests" 62 | import json 63 | import logging 64 | 65 | from use import auto_install, fatal_exceptions, no_cleanup, use 66 | from use.hash_alphabet import JACK_as_num, hexdigest_as_JACK, is_JACK, num_as_hexdigest 67 | from use.pimp import _parse_name 68 | from use.pydantics import JustUse_Info, PyPI_Project, PyPI_Release, Version 69 | 70 | log = logging.getLogger(".".join((__package__, __name__))) 71 | log.setLevel(logging.DEBUG if "DEBUG" in os.environ else logging.NOTSET) 72 | 73 | use.config.testing = True 74 | 75 | 76 | @fixture() 77 | def reuse(): 78 | """ 79 | Return the `use` module in a clean state for "reuse." 80 | 81 | NOTE: making a completely new one each time would take 82 | ages, due to expensive _registry setup, re-downloading 83 | venv packages, etc., so if we are careful to reset any 84 | additional state changes on a case-by-case basis, 85 | this approach is more efficient and is the clear winner. 86 | """ 87 | use._using.clear() 88 | use.main._reloaders.clear() 89 | return use 90 | 91 | 92 | p = Path(__file__).parent / "beast_data.json" 93 | 94 | with open(p) as file: 95 | data = json.load(file) 96 | 97 | begin = time() 98 | 99 | 100 | @mark.parametrize("package_name, module_name, version", data) 101 | def test_mass(reuse, package_name, module_name, version): 102 | """ 103 | Taken from the original beast test suite, and boiled down to the bare minimum. 104 | """ 105 | with patch("webbrowser.open"), io.StringIO() as buf, redirect_stdout(buf): 106 | try: 107 | mod = reuse(package_name, version=version, modes=reuse.auto_install) 108 | assert False, f"Actually returned mod: {mod}" 109 | except RuntimeWarning: 110 | recommended_hash = buf.getvalue().splitlines()[-1].strip() 111 | mod = reuse( 112 | package_name=package_name, 113 | module_name="", 114 | version=version, 115 | hashes=recommended_hash, 116 | modes=reuse.auto_install | reuse.no_cleanup, 117 | ) 118 | assert isinstance(mod, ModuleType) 119 | 120 | 121 | print("============================================================") 122 | print("ran", len(data), "tests in", (time() - begin) // 60, "minutes") 123 | -------------------------------------------------------------------------------- /tests/beast_data_preparation.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from packaging.version import Version as PkgVersion, InvalidVersion 3 | from typing import Optional, Union 4 | 5 | import json 6 | 7 | class Version(PkgVersion): 8 | def __new__(cls, *args, **kwargs): 9 | if args and isinstance(args[0], Version): 10 | return args[0] 11 | else: 12 | return super(cls, Version).__new__(cls) 13 | 14 | def __init__(self, versionobj: Optional[Union[PkgVersion, str]] = None, *, major=0, minor=0, patch=0): 15 | if isinstance(versionobj, Version): 16 | return 17 | 18 | if versionobj: 19 | try: 20 | super(Version, self).__init__(versionobj) 21 | except InvalidVersion: 22 | super(Version, self).__init__("0") 23 | return 24 | 25 | if major is None or minor is None or patch is None: 26 | raise ValueError( 27 | f"Either 'Version' must be initialized with either a string, packaging.version.Verson, {__class__.__qualname__}, or else keyword arguments for 'major', 'minor' and 'patch' must be provided. Actual invocation was: {__class__.__qualname__}({versionobj!r}, {major=!r}, {minor=!r})" 28 | ) 29 | 30 | # string as only argument 31 | # no way to construct a Version otherwise - WTF 32 | versionobj = ".".join(map(str, (major, minor, patch))) 33 | super(Version, self).__init__(versionobj) 34 | 35 | def __iter__(self): 36 | yield from self.release 37 | 38 | def __repr__(self): 39 | return f"Version('{super().__str__()}')" 40 | 41 | def __hash__(self): 42 | return hash(self._version) 43 | 44 | @classmethod 45 | def __get_validators__(cls): 46 | yield cls.validate 47 | 48 | @classmethod 49 | def validate(cls, value): 50 | return Version(value) 51 | 52 | p = (Path(__file__).parent.absolute() / "../integration/pypi.json") 53 | 54 | with open(p) as file: 55 | data = json.load(file)["data"] 56 | 57 | DATA = [(d["name"], d["name"], str(max(Version(v) for v in d["versions"]))) for d in data] 58 | 59 | p = (Path(__file__).parent.absolute() / "../beast_data.json").resolve() 60 | with open(p, "w") as file: 61 | json.dump(DATA, file, indent=4, sort_keys=True) 62 | -------------------------------------------------------------------------------- /tests/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o allexport 3 | arg1="$1" 4 | 5 | [ -d use -a -f use/use.py ] && cd .. 6 | [ -f unit_teat.py ] && cd .. 7 | while [ ! -e requirements.txt ]; do 8 | cd .. 9 | done 10 | 11 | eval "cov_file_opts=($( find -name "*.py" | sed -r -e 's~^\./~~; s~\.py$~~; s~[_-][^_/.-]*\..*$~~; s~/~.~g; s~^src\.~~; s~^~--cov=~;' | sort | tr -s '\n ' " "; ))" 12 | 13 | : ${PYTHON:=python3} 14 | [ "x${PYTHON: 0:1}" = "x/" ] || PYTHON="$( which ""$PYTHON"" || which "python" || which "python.exe" )" 15 | export PYTHON 16 | 17 | which apt && ! which busybox && { apt install -y busybox || sudo apt install -y busybox; } || true 18 | 19 | 20 | echo "=== COVERAGE SCRIPT ===" 21 | echo "$0 running from $( pwd )" 22 | coveragerc_cdir="$( pwd )" 23 | coveragerc_cfile="$coveragerc_cdir/.coveragerc" 24 | while [ ! -e "$coveragerc_cfile" ]; do 25 | coveragerc_cdir="${coveragerc_cfile%/?*}" 26 | coveragerc_cdir2="${coveragerc_cdir%/?*}" 27 | [ "x${coveragerc_cfile2##/}" = "x/" ] && echo "No coveragerc" && exit 255 28 | coveragerc_cfile="$( find "$coveragerc_cdir2" -mindepth 2 -maxdepth 4 -name .coveragerc )" 29 | done 30 | coveragerc_cdir="${coveragerc_cfile%/?*}" 31 | 32 | echo "coveragerc_cfile=${coveragerc_cfile}" 1>&2 33 | echo "coveragerc_cdir=${coveragerc_cdir}" 1>&2 34 | ! cd "$coveragerc_cdir" && echo "Failed to cd to \$coveragerc_dir [\"$coveragerc_dir\"]" 1>&2 && exit 255 35 | 36 | 37 | 38 | IFS=$'\n'; badge_urls=($( curl -ks "https://raw.githubusercontent.com/amogorkon/justuse/main/README.md" | grep -e '\[!\[coverage\]' | tr -s '()' '\n' | grep -Fe "://" | grep -Fe ".svg" )); badge_url_no_query="${badge_urls[0]%%[#\?]*}"; badge_fn="${badge_url_no_query##*/}"; BADGE_FILENAME="$badge_fn"; 39 | default="/home/runner/work/justuse/justuse/$BADGE_FILENAME" 40 | f="$default"; fn="${f##*/}"; dir="${f: 0:${#f}-${#fn}}"; dir="${dir%%/}" 41 | [ ! -d "$dir" ] && default="$PWD/coverage/$BADGE_FILENAME" 42 | file="${COVERAGE_IMAGE:-${default}}" 43 | f="$file"; fn="${f##*/}"; dir="${f: 0:${#f}-${#fn}}"; dir="${dir%%/}"; nme="${fn%.*}" 44 | covcom=( --cov-branch --cov-report term-missing --cov-report html:coverage/ --cov-report annotate:coverage/annotated --cov-report xml:coverage/cov.xml ) 45 | covsrc=( "${cov_file_opts[@]}" ) 46 | 47 | echo "default=$default" 1>&2 48 | echo "COVERAGE_IMAGE=$COVERAGE_IMAGE" 1>&2 49 | echo "file=$file" 1>&2 50 | 51 | opts=( -v ) 52 | for append in 1; do 53 | 54 | if (( append > 0 )); then 55 | opts+=( --cov-append ) 56 | opts+=( -vv ) 57 | fi 58 | 59 | 60 | "$PYTHON" -m pytest "${covcom[@]}" "${covsrc[@]}" "${opts[@]}" "$@" 61 | rs=$? 62 | 63 | (( rs )) && exit $rs 64 | done 65 | set +e 66 | 67 | if [ ! -f .coveragerc ]; then 68 | echo "Cannot find .coveragerc after mpving to $coveragerc_cdir: $(pwd)" 69 | find "$( pwd )" -printf '%-12s %.10T@ %p\n' | sort -k2n 70 | exit 123 71 | fi 72 | 73 | ls -lAp --color=always 74 | find "$coveragerc_cdir" -mindepth 2 -name ".coverage" -printf '%-12s %.10T@ %p\n' \ 75 | | sort -k1n | cut -c 25- | tail -n 1 \ 76 | | { 77 | max=0 78 | IFS=$'\n'; while read -r coveragerc_cf; do 79 | f="$coveragerc_cf"; fn="${f##*/}" 80 | dir="${f: 0:${#f}-${#fn}}"; dir="${dir%%/}" 81 | ((max += 1)) 82 | name=".coverage.$max" 83 | (( max == 1 )) && name="${name%%[!0-9]1}" 84 | echo -E "Copying cov data from $f to $( pwd )/$name" 1>&2 85 | cp -vf -- "$f" "./$name" 1>&2 86 | cp -vf -- "$f" "./.coverage" 1>&2 87 | done; 88 | } 89 | 90 | 91 | mkdir -p coverage 92 | cp -vf -- .coverage coverage/.coverage 93 | 94 | mkdir -p "$dir" 95 | 96 | bash tests/coverage_badge.sh | tee "$file" 97 | echo "Found an image to publish: [$file]" 1>&2 98 | orig_file="$file" 99 | 100 | IFS=$'\n'; remotes=($( git remote -v | cut -f2 | cut -d: -f2 | cut -d/ -f1 | sort | uniq )) 101 | 102 | branch="$( git branch -v | grep -Fe "*" | head -1 | cut -d " " -f2; )" 103 | badge_filenames=( ) 104 | for remote in "${remotes[@]}"; do 105 | badge_filename="coverage_${remote}-${branch}.svg" 106 | badge_filenames+=( "$badge_filename" ) 107 | done 108 | 109 | if [ "x$FTP_USER" != "x" ]; then 110 | for filename in "$orig_file" "${badge_filenames[@]}"; do 111 | f="$file"; fn="${f##*/}"; dir="${f: 0:${#f}-${#fn}}"; dir="${dir%%/}"; _dir="$dir"; f="$filename"; fn="${f##*/}"; dir="${f: 0:${#f}-${#fn}}"; dir="${dir%%/}"; _fn="$fn"; f="$file"; fn="${f##*/}" 112 | fn="${filename##*/}" 113 | rm -vf -- "$file" || rmdir "$file"; 114 | bash tests/coverage_badge.sh | cat -v | tee "$_dir/$_fn" | tee "$file"; 115 | for variant in \ 116 | '"/public_html/mixed/$fn" "$file"' \ 117 | '"/public_html/mixed/coverage_amogorkon-main.svg" "$file"' \ 118 | '"/public_html/mixed/coverage.svg" "$file"' \ 119 | ; \ 120 | do 121 | eval "set -- $variant" 122 | cmd=( ftpput -v -P 21 -u "$FTP_USER" -p "$FTP_PASS" \ 123 | ftp.pinproject.com "$@" ) 124 | echo -E "Trying variant:" 1>&2 125 | if (( ! UID )); then 126 | echo "$@" 1>&2 127 | fi 128 | command "${cmd[@]}" 129 | rs=$? 130 | if (( ! rs )); then 131 | break 132 | fi 133 | done 134 | [ $rs -eq 0 ] && echo "*** Image upload succeeded: $@ ***" 1>&2 135 | done 136 | fi 137 | 138 | exit ${rs:-0} # upload 139 | 140 | -------------------------------------------------------------------------------- /tests/coverage_badge.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | COV_PC="$( find -name "index.html" -a -path "*/coverage[!a-zA-Z]*" -exec grep -A5 -Fe "Coverage report:" -- "{}" + | sed -r -e ' \~^.*class="pc_cov">([^<>]+).*$~!d; s~~\1~; s~%~~; tk; d; :k ' | sort -k1n | tail -1 )" 4 | cov_grn=$(( COV_PC * 2550 / 1000)); cov_grn_hex="$( printf "%02x" $cov_grn; )" 5 | cat < 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | coverage 22 | coverage 23 | ${COV_PC}% 24 | ${COV_PC}% 25 | 26 | 27 | EOF1 28 | 29 | -------------------------------------------------------------------------------- /tests/coverage_combine.sh: -------------------------------------------------------------------------------- 1 | #!/system/bin/sh 2 | 3 | 4 | IFS=$'\n'; 5 | cov_dbs=($( find "$( pwd )" -type f -exec file -npsNzF\| "{}" + | grep --line-buffered -E 'SQLite ' | grep -vFe "coverage2.db" | cut -d\| -f1 | xargs -d $'\n' md5sum | seen_new -e '$1' -c 'substr($0, length($1)+3)' )); 6 | IFS=$'\n'; 7 | tables=($( sqlite3 "${cov_dbs[0]}" '.schema' | sed -r -e ' 8 | \~^.*CREATE TABLE[\t ]*([\t ]+IF NOT EXISTS|)[\t ]+(['"'"'"]?)([a-zA-Z0-9_.]+)\2($|[^a-zA-Z0-9_.].*)$~{ s~~\3~p; 9 | };d'; )); ntbls=${#tables[@]}; 10 | ndbs=${#cov_dbs[@]}; 11 | dbidx=-1; 12 | while (( ++dbidx < ndbs )); do 13 | db="${cov_dbs[dbidx]}"; 14 | echo "Reading from $db ..." 1>&2; (( dbidx == 0 )) && sqlite3 "$db" '.schema' | sed -r -e ' 15 | s~(^|$|['"'"'"\t\r\n ;])--.*$~\1~;' | tr -s '\t\r\n ' ' ' | tr -s ';' '\n' | grep -Eie 'CREATE TABLE' | trim | add_suffix ';' | tee ./0_0__schema.sql 1>&2; 16 | tblidx=-1; 17 | ntbls=${#tables[@]}; 18 | while (( ++tblidx < ntbls )); do 19 | tbl="${tables[tblidx]}"; 20 | echo -E " - Load data from table $tbl" 1>&2; { echo -nE "INSERT OR IGNORE INTO $tbl "; 21 | sqlite3 "${db}" -tabs "select * from $tbl" -separator $'\xf8' -newline $'\f' | sed -r -e ' 22 | s~'"'"'~\xf9~g; :a s~(\xf8|^)([^\n\xf8]*)($|\xf8)~'"'"'\n '"'"'\2'"'"',\n \3~g; ta; 23 | s~\f~'"'"',\n), (\n '"'"'~g; ta; 24 | s~(^|\n +\xf8)([^\n\xf8]+[^'"'"'\\\n\xf8])\n +'"'"'~\n '"'"'\2'"'"',\n ~g; ta; 25 | s~\xf8~'"'"'~g; 26 | s~, *\([^()a-zA-Z0-9_]*$~;\n~; 27 | s~^[^\n]*\n~VALUES (\n~; 28 | s~\xf9~'"'"' + "'"'"'" + '"'"'~g; ' | sed -r -e '\~^ +'"'"'(.*)'"'"' *$~{ N; \~\n +'"'"'~{ s~~,&~; 29 | }; }; \~^ +'"'"'[\t\r ]*$~{ d; ' -e ' ; 30 | }; ' | tr -s '\n' '\v' | sed -r -e ':a s~,([\t\n\v\r ]*)(;|$|\))~\1\2~g; ta;' | tr -s '\v' '\n'; 31 | } > "${dbidx}_${tblidx}_${tbl}.sql"; 32 | grep -qFe "VALUES" --text -hs -- "${dbidx}_${tblidx}_${tbl}.sql" || rm -vf -- "${dbidx}_${tblidx}_${tbl}.sql" 1>&2; 33 | done; 34 | done; 35 | rm coverage2.db; 36 | for f in ./0_*.sql ./[1-9]*.sql; do 37 | [ -f "$f" ] || continue 38 | fn="${f##*/}"; 39 | nme="${fn%.*}"; 40 | tbl="${nme##*[0-9]_}"; 41 | sqlite3 coverage2.db -init "$f" "; VACUUM; REINDEX;"; 42 | echo "[ $? ] $f"; 43 | done 44 | sqlite3 coverage2.db "VACUUM; 45 | DELETE FROM coverage_schema; 46 | insert into coverage_schema VALUES (7); VACUUM; REINDEX; " 47 | cat coverage2.db > .coverage 48 | 49 | -------------------------------------------------------------------------------- /tests/foo.py: -------------------------------------------------------------------------------- 1 | use: callable 2 | 3 | 4 | def bar(): 5 | return use("math").sqrt(4) == 2 6 | -------------------------------------------------------------------------------- /tests/integration/README.md: -------------------------------------------------------------------------------- 1 | # Mass Testing 2 | 3 | To run the mass tests: 4 | 5 | ```bash 6 | docker-compose up --build -d 7 | docker exec -it justuse-scraper bash 8 | 9 | # To recreate the pypi.json cache: 10 | python collect_packages.py 11 | 12 | # To test the packages: 13 | python justtest.py 14 | ``` 15 | 16 | ## TODO 17 | 18 | - Add in file size into the cache - currently tensorflow is at the top with stars and is huge... 19 | - Tidy up code, document the intention and how users can add their own packages into the tests 20 | - Add some unit tests to ensure the mass test functionality is working 21 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | 3 | from pathlib import Path 4 | src = import_base = Path(__file__).parent / "../../src" 5 | cwd = Path().cwd() 6 | os.chdir(src) 7 | sys.path.insert(0, "") if "" not in sys.path else None 8 | 9 | if sys.version_info < (3, 9) and "use" not in sys.modules: 10 | import gc, types, typing 11 | from typing import _GenericAlias as GenericAlias 12 | from abc import ABCMeta 13 | from collections.abc import Callable 14 | from types import CellType 15 | for t in (list, dict, set, tuple, frozenset, ABCMeta, Callable, CellType): 16 | r = gc.get_referents(t.__dict__)[0] 17 | r.update( 18 | { 19 | "__class_getitem__": classmethod(GenericAlias), 20 | } 21 | ) 22 | 23 | 24 | import use 25 | os.chdir(cwd) -------------------------------------------------------------------------------- /tests/integration/collect_packages.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import time 4 | from typing import Optional 5 | 6 | import requests 7 | from bs4 import BeautifulSoup 8 | from pydantic import BaseModel 9 | 10 | GITHUB_TOKEN = os.environ["GITHUB_TOKEN"] 11 | 12 | 13 | def get_soup(page: int): 14 | url = "https://anaconda.org/anaconda/repo?sort=time.modified&access=public&sort_order=desc&type=all&page={page}" 15 | return BeautifulSoup(requests.get(url.format(page=page)).text, features="html.parser") 16 | 17 | 18 | def parse_package(soup): 19 | return { 20 | "name": soup.select_one("span.packageName").text, 21 | "href": soup.select_one("a[data-package]")["href"], 22 | } 23 | 24 | 25 | def parse_packages(soup): 26 | return [parse_package(s) for s in soup.select("td.pkg-title")] 27 | 28 | 29 | def find_all_package_names(): 30 | 31 | home = get_soup(page=1) 32 | number_of_pages = int(home.select_one("ul.pagination li[aria-disabled] a").text.split()[-1]) 33 | 34 | packages = parse_packages(home) 35 | for page in range(2, number_of_pages + 1): 36 | soup = get_soup(page=page) 37 | packages += parse_packages(soup) 38 | 39 | return packages 40 | 41 | 42 | def optional_text(soup, default=""): 43 | if soup is None: 44 | return default 45 | return soup.text 46 | 47 | 48 | def find_meta(pkg: dict[str, str]): 49 | url = f"https://pypi.org/pypi/{pkg['name']}/json" 50 | r = requests.get(url) 51 | if r.status_code != 200: 52 | return 53 | meta = r.json() 54 | link_options = [meta["info"]["home_page"]] 55 | if (project_urls := meta["info"].get("project_urls")) is not None: 56 | link_options += list(project_urls.values()) 57 | owner, repo, url = get_github(link_options) 58 | stars = get_stars(owner, repo) 59 | base = {"name": pkg["name"], "versions": [version for version in meta["releases"].keys()]} 60 | if stars < 0: 61 | return base 62 | return {**base, **{"stars": stars, "repo": url}} 63 | 64 | 65 | def get_github(urls: list[str]): 66 | for url in urls: 67 | if not isinstance(url, str): 68 | continue 69 | url = url.strip().strip("/") 70 | base1 = "https://github.com/" 71 | base2 = "http://github.com/" 72 | paths = [] 73 | if url.startswith(base1): 74 | paths = url[len(base1) :].split("/") 75 | base = base1 76 | if url.startswith(base2): 77 | paths = url[len(base2) :].split("/") 78 | base = base2 79 | 80 | if len(paths) >= 2: 81 | return paths[0], paths[1], base + paths[0] + "/" + paths[1] 82 | 83 | return None, None, None 84 | 85 | 86 | def get_stars(owner: str, repo: str): 87 | if owner is None or repo is None: 88 | return -1 89 | 90 | query = f"""query {{ 91 | repository(owner: "{owner}", name: "{repo}") {{ 92 | stargazers {{ 93 | totalCount 94 | }} 95 | }} 96 | }}""" 97 | r = requests.post( 98 | "https://api.github.com/graphql", 99 | json={"query": query}, 100 | headers={"Authorization": f"token {GITHUB_TOKEN}"}, 101 | ) 102 | if r.status_code == 200: 103 | data = r.json() 104 | try: 105 | if data is None: 106 | print("Hit GitHub rate limit, sleeping") 107 | time.sleep(60) 108 | return get_stars(owner, repo) 109 | return data["data"]["repository"]["stargazers"]["totalCount"] 110 | except: 111 | print(data) 112 | return -1 113 | 114 | print(r.status_code) 115 | raise Exception(r.text) 116 | 117 | 118 | def try_to_get_github_stars(pkg): 119 | owner, repo = get_github([pkg["urls"]["dev"], pkg["urls"]["home"]]) 120 | if owner is None: 121 | return -1 122 | return get_stars(owner, repo) 123 | 124 | 125 | def main(): 126 | 127 | ## Step 1 - get all conda pkg names and dump to file 128 | # with open("tmp.json", "r") as f: 129 | # packages = json.load(f) 130 | packages = find_all_package_names() 131 | 132 | ## Step 2 - go to pypi and find metadata (try to get stars from github) 133 | pypi_packages = Packages() 134 | for i, pkg in enumerate(packages): 135 | meta = find_meta(pkg) 136 | if meta is None: 137 | print("Not on Pypi", pkg) 138 | continue 139 | pypi_packages.append(PackageToTest(**meta)) 140 | print(i) 141 | 142 | ## Step 4 Dump out 143 | with open("pypi.json", "w") as f: 144 | json.dump(pypi_packages.dict(), f, indent=2, sort_keys=True) 145 | 146 | 147 | class PackageToTest(BaseModel): 148 | name: str 149 | versions: list[str] 150 | repo: Optional[str] = None 151 | stars: Optional[int] = None 152 | 153 | 154 | class Packages(BaseModel): 155 | data: list[PackageToTest] = [] 156 | 157 | def append(self, item: PackageToTest) -> None: 158 | self.data.append(item) 159 | 160 | 161 | if __name__ == "__main__": 162 | main() 163 | -------------------------------------------------------------------------------- /tests/integration/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | justuse-scraper: 4 | build: . 5 | image: challisa/justuse-scraper 6 | container_name: justuse-scraper 7 | volumes: 8 | - .:/opt/working 9 | command: tail -f /dev/null 10 | -------------------------------------------------------------------------------- /tests/integration/dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | 3 | COPY requirements.txt . 4 | 5 | RUN pip install -r requirements.txt 6 | 7 | WORKDIR /opt/working -------------------------------------------------------------------------------- /tests/integration/integration_test.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import os 3 | import re 4 | import sys 5 | import tempfile 6 | import warnings 7 | from collections.abc import Callable 8 | from contextlib import closing 9 | from importlib.machinery import SourceFileLoader 10 | from pathlib import Path 11 | from threading import _shutdown_locks 12 | 13 | import packaging.tags 14 | import packaging.version 15 | import pytest 16 | 17 | if Path("src").is_dir(): 18 | sys.path.insert(0, "") if "" not in sys.path else None 19 | lpath, rpath = ( 20 | sys.path[: sys.path.index("") + 1], 21 | sys.path[sys.path.index("") + 2 :], 22 | ) 23 | 24 | try: 25 | sys.path.clear() 26 | sys.path.__iadd__(lpath + [os.path.join(os.getcwd(), "src")] + rpath) 27 | import use 28 | finally: 29 | sys.path.clear() 30 | sys.path.__iadd__(lpath + rpath) 31 | import_base = Path(__file__).parent.parent / "src" 32 | is_win = sys.platform.startswith("win") 33 | import use 34 | 35 | __package__ = "tests" 36 | from tests.unit_test import reuse, ScopedCwd 37 | 38 | import logging 39 | 40 | log = logging.getLogger(".".join((__package__, __name__))) 41 | log.setLevel(logging.DEBUG if use.config.debugging else logging.NOTSET) 42 | 43 | 44 | params = [ 45 | # ("olefile", "0.46"), # Windows-only 46 | ("workerpool", "0.9.4"), 47 | ("fastcache", "1.1.0"), 48 | ("pytest-cov", "2.12.1"), 49 | ("pytest-env", "0.6.2"), 50 | ("requests", "2.24.0"), 51 | ("furl", "2.1.2"), 52 | ("wheel", "0.36.2"), 53 | ("icontract", "2.5.4"), 54 | ("tiledb", "0.9.5"), 55 | ("wurlitzer", "3.0.2"), 56 | # ("cctools", "7.0.17"), # too slow, takes minutes to build 57 | ("clang", "9.0"), 58 | ] 59 | 60 | 61 | @pytest.mark.parametrize("name,version", params) 62 | def test_sample(reuse, name, version): 63 | try: 64 | reuse(name, version=version, modes=reuse.auto_install) 65 | except BaseException as ie: 66 | suggestion = ie.args[0].strip().splitlines()[-1] 67 | log.debug("suggestion = %s", repr(suggestion)) 68 | mod = eval(suggestion) 69 | assert mod 70 | return 71 | assert False, "Should raise ImportError: missing hashes." 72 | 73 | 74 | @pytest.mark.parametrize("name, version", (("numpy", "1.19.3"),)) 75 | def test_86_numpy(reuse, name, version): 76 | use = reuse # for the eval() later 77 | with pytest.raises(RuntimeWarning) as w: 78 | reuse(name, version=version, modes=reuse.auto_install) 79 | assert w 80 | recommendation = str(w.value).split("\n")[-1].strip() 81 | mod = eval(recommendation) 82 | assert mod.__name__ == reuse._parse_name(name)[1] 83 | return mod # for the redownload test 84 | 85 | 86 | def test_redownload_module(reuse): 87 | def inject_fault(*, path, **kwargs): 88 | log.info("fault_inject: deleting %s", path) 89 | path.delete() 90 | 91 | assert test_86_numpy(reuse, "example-pypi-package/examplepy", "0.1.0") 92 | try: 93 | reuse.fault_inject = inject_fault 94 | assert test_86_numpy(reuse, "example-pypi-package/examplepy", "0.1.0") 95 | finally: 96 | del reuse.fault_inject 97 | assert test_86_numpy(reuse, "example-pypi-package/examplepy", "0.1.0") 98 | 99 | 100 | def double_function(func): 101 | import functools 102 | 103 | @functools.wraps(func) 104 | def wrapper(*args, **kwargs): 105 | return func(*args, **kwargs) * 2 106 | 107 | return wrapper 108 | 109 | 110 | def test_aspectize_defaults(reuse): 111 | # baseline 112 | srcdir = Path(__file__).parent.parent.parent 113 | if "tests.simple_funcs" in sys.modules: 114 | del sys.modules["tests.simple_funcs"] 115 | with ScopedCwd(srcdir): 116 | mod = reuse(reuse.Path("./tests/simple_funcs.py"), package_name="tests") 117 | assert mod.two() == 2 118 | 119 | 120 | def test_aspectize_function_by_name(reuse): 121 | # functions with specific names only 122 | srcdir = Path(__file__).parent.parent.parent 123 | if "tests.simple_funcs" in sys.modules: 124 | del sys.modules["tests.simple_funcs"] 125 | with ScopedCwd(srcdir): 126 | mod = reuse(reuse.Path("./tests/simple_funcs.py"), package_name="tests") @ ( 127 | reuse.isfunction, 128 | "two", 129 | double_function, 130 | ) 131 | assert mod.two() == 4 132 | assert mod.three() == 3 133 | assert reuse.ismethod 134 | 135 | 136 | def test_aspectize_all_functions(reuse): 137 | # all functions, but not classes or methods 138 | srcdir = Path(__file__).parent.parent.parent 139 | if "tests.simple_funcs" in sys.modules: 140 | del sys.modules["tests.simple_funcs"] 141 | with ScopedCwd(srcdir): 142 | mod = reuse(reuse.Path("./tests/simple_funcs.py"), package_name="tests") @ ( 143 | reuse.isfunction, 144 | "", 145 | double_function, 146 | ) 147 | assert mod.two() == 4 148 | assert mod.three() == 6 149 | inst = mod.Two() 150 | assert inst() == 2 151 | inst = mod.Three() 152 | assert inst.three() == 3 153 | 154 | 155 | def test_simple_url(reuse): 156 | import http.server 157 | 158 | port = 8089 159 | orig_cwd = Path.cwd() 160 | try: 161 | os.chdir(Path(__file__).parent.parent.parent) 162 | 163 | with http.server.HTTPServer(("", port), http.server.SimpleHTTPRequestHandler) as svr: 164 | foo_uri = f"http://localhost:{port}/tests/.tests/foo.py" 165 | print(f"starting thread to handle HTTP request on port {port}") 166 | import threading 167 | 168 | thd = threading.Thread(target=svr.handle_request) 169 | thd.start() 170 | print(f"loading foo module via use(URL({foo_uri}))") 171 | with pytest.warns(use.NoValidationWarning): 172 | mod = reuse(reuse.URL(foo_uri), initial_globals={"a": 42}) 173 | assert mod.test() == 42 174 | finally: 175 | os.chdir(orig_cwd) 176 | 177 | 178 | def test_autoinstall_numpy_dual_version(reuse): 179 | ver1, ver2 = "1.19.3", "1.19.5" 180 | for ver in (ver1, ver2): 181 | for k, v in list(sys.modules.items()): 182 | if k == "numpy" or k.startswith("numpy."): 183 | loader = getattr(v, "__loader__", None) or v.__spec__.loader 184 | if isinstance(loader, SourceFileLoader): 185 | del sys.modules[k] 186 | try: 187 | mod = suggested_artifact(reuse, "numpy", version=ver) 188 | assert mod 189 | assert mod.__version__ == ver 190 | except RuntimeError: 191 | pass 192 | 193 | 194 | def test_autoinstall_protobuf(reuse): 195 | ver = "3.19.1" 196 | mod = suggested_artifact(reuse, "protobuf/google.protobuf", version=ver) 197 | assert mod.__version__ == ver 198 | assert mod.__name__ == "google.protobuf" 199 | assert tuple(Path(mod.__file__).parts[-3:]) == ("google", "protobuf", "__init__.py") 200 | 201 | 202 | def suggested_artifact(reuse, *args, **kwargs): 203 | reuse.pimp._clean_sys_modules(args[0].split("/")[-1].split(".")[0]) 204 | try: 205 | mod = reuse(*args, modes=reuse.auto_install | reuse.Modes.fastfail, **kwargs) 206 | return mod 207 | except RuntimeWarning as rw: 208 | last_line = str(rw).strip().splitlines()[-1].strip() 209 | log.info("Usimg last line as suggested artifact: %s", repr(last_line)) 210 | last_line2 = last_line.replace("protobuf", "protobuf/google.protobuf") 211 | mod = eval(last_line2) 212 | log.info("suggest artifact returning: %s", mod) 213 | return mod 214 | -------------------------------------------------------------------------------- /tests/integration/justtest.py: -------------------------------------------------------------------------------- 1 | import json 2 | from os import PathLike 3 | import subprocess 4 | import shutil 5 | from pathlib import Path 6 | 7 | import use 8 | from test_single import Packages 9 | 10 | 11 | def manage_disk(max_size=5_000_000_000): 12 | if not (use.home / "venv").exists(): 13 | return 14 | if not any((use.home / "venv").iterdir()): 15 | return 16 | current_usage = int(subprocess.check_output(["du", "-sb", f"{use.home}/venv"]).split(b"\t")[0]) 17 | if current_usage > max_size: 18 | process = subprocess.Popen(f"du -sb {use.home}/venv/* | sort -n -r", shell=True, stdout=subprocess.PIPE) 19 | venv_usages = process.communicate()[0].split(b"\n") 20 | for venv in venv_usages: 21 | try: 22 | size, path = venv.split(b"\t") 23 | path = path.decode() 24 | size = int(size) 25 | venv_package = path.split("/")[-1] 26 | 27 | print(f"Deleting {venv_package} to make extra space, freed {size/1_000_000} MB") 28 | shutil.rmtree(path) 29 | current_usage -= size 30 | if current_usage < max_size: 31 | break 32 | except: 33 | continue 34 | 35 | 36 | def clear_cache(): 37 | results_dir = Path("results") 38 | if results_dir.exists(): 39 | shutil.rmtree("results") 40 | 41 | 42 | def run_test(packages: Packages, results_dir: PathLike, max_to_run: int = 1, max_venv_space: int = 5_000_000_000): 43 | for i, pkg in enumerate(packages.data): 44 | if i >= max_to_run: 45 | break 46 | if pkg.name in NAUGHTY_PACKAGES: 47 | continue 48 | 49 | manage_disk(max_size=max_venv_space) 50 | 51 | subprocess.call(f"python test_single.py {i}", shell=True) 52 | n_passed = len(list((results_dir / "pass").glob("*.json"))) 53 | n_failed = len(list((results_dir / "fail").glob("*.json"))) 54 | print(i, pkg.name, n_failed + n_passed, n_failed, n_passed, f"{100 * n_passed / (n_failed + n_passed)}%") 55 | 56 | 57 | def combine_package_output(results_dir: PathLike, folder: str): 58 | 59 | packages = [] 60 | for file_path in (results_dir / folder).glob("*.json"): 61 | with open(file_path, "r") as f: 62 | packages.append(json.load(f)) 63 | 64 | with open(f"{folder}.json", "w") as f: 65 | json.dump(packages, f, indent=2, sort_keys=True) 66 | 67 | return packages 68 | 69 | 70 | if __name__ == "__main__": 71 | NAUGHTY_PACKAGES = ["assimp", "metakernel", "pscript", "airflow", "tensorflow", "tensorflow-gpu"] 72 | 73 | with open("pypi.json", "r") as f: 74 | packages = Packages(data=json.load(f)["data"]) 75 | 76 | packages.data.sort(key=lambda p: p.stars or 0, reverse=True) 77 | 78 | results_dir = Path("results") 79 | 80 | clear_cache() 81 | run_test(packages, results_dir, 100, max_venv_space=5_000_000_000) 82 | passed = combine_package_output(results_dir, "pass") 83 | failed = combine_package_output(results_dir, "fail") 84 | clear_cache() 85 | 86 | print("Total: ", len(packages.data)) 87 | print("Failed: ", len(failed)) 88 | print("Passed: ", len(passed)) 89 | print("Pass rate: ", 100 * (len(passed) / len(failed + passed))) 90 | -------------------------------------------------------------------------------- /tests/integration/requirements.txt: -------------------------------------------------------------------------------- 1 | asttokens==2.0.5 2 | beautifulsoup4==4.10.0 3 | bs4==4.10.0 4 | certifi==2021.10.8 5 | charset-normalizer==2.0.7 6 | furl==2.1.3 7 | icontract==2.5.5 8 | idna==3.3 9 | justuse==0.5.0 10 | multidict==5.2.0 11 | orderedmultidict==1.0.1 12 | packaging==21.0 13 | pydantic==1.8.2 14 | pyparsing==2.4.7 15 | requests==2.26.0 16 | six==1.16.0 17 | soupsieve==2.2.1 18 | toml==0.10.2 19 | typeguard==2.13.0 20 | typing-extensions==3.10.0.2 21 | urllib3==1.26.7 22 | yarl==1.7.0 -------------------------------------------------------------------------------- /tests/integration/test_pypi_model.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | import traceback 4 | 5 | import requests 6 | from use.pydantics import PyPI_Project 7 | 8 | with open("pypi.json", "r") as f: 9 | packages = json.load(f) 10 | 11 | 12 | for i, pkg in enumerate(packages["data"]): 13 | r = requests.get(f"https://pypi.org/pypi/{pkg['name']}/json") 14 | try: 15 | project = PyPI_Project(**r.json()) 16 | filtered = project.filter_by_version_and_current_platform(project.info.justuse.version) 17 | for version in filtered.releases.values(): 18 | for release in version: 19 | print(release.filename) 20 | print(release.justuse) 21 | except: 22 | exc_type, exc_value, _ = sys.exc_info() 23 | tb = traceback.format_exc() 24 | fail = { 25 | "name": pkg["name"], 26 | "err": { 27 | "type": str(exc_type), 28 | "value": str(exc_value), 29 | "traceback": tb.split("\n"), 30 | }, 31 | } 32 | 33 | print(json.dumps(fail, indent=2, sort_keys=True)) 34 | break 35 | 36 | if i > 4: 37 | break 38 | -------------------------------------------------------------------------------- /tests/integration/test_single.py: -------------------------------------------------------------------------------- 1 | import io 2 | import json 3 | import logging 4 | import os 5 | import re 6 | import sys 7 | import traceback 8 | from pathlib import Path 9 | from typing import Optional 10 | 11 | import packaging 12 | 13 | sys.path.insert(1, str((Path(__file__).parent.parent.parent / "src").absolute())) 14 | use_py = Path(__file__).parent.parent.parent / "src" / "use" / "use.py" 15 | assert use_py.exists() 16 | from importlib.machinery import SourceFileLoader 17 | 18 | ldr = SourceFileLoader("use", use_py.parent) 19 | __import__("use") 20 | use_py = Path(__file__).parent.parent.parent / "src" / "use" / "use.py" 21 | assert use_py.exists() 22 | print(Path(__file__).parent.parent.parent / "src") 23 | sys.path.append(str(Path(__file__).parent.parent.parent / "src")) 24 | os.chdir(Path(__file__).parent) 25 | import use 26 | from pydantic import BaseModel 27 | 28 | 29 | def start_capture_logs(): 30 | log_capture_string = io.StringIO() 31 | ch = logging.StreamHandler(log_capture_string) 32 | ch.setLevel(logging.DEBUG) 33 | logging.root.handlers = [ch] 34 | return log_capture_string 35 | 36 | 37 | def get_capture_logs(log_capture_string): 38 | log_contents = log_capture_string.getvalue() 39 | log_capture_string.close() 40 | return log_contents 41 | 42 | 43 | class PackageToTest(BaseModel): 44 | name: str 45 | versions: list[str] 46 | repo: Optional[str] = None 47 | stars: Optional[int] = None 48 | 49 | @property 50 | def safe_versions(self): 51 | return list(filter(lambda k: k.replace(".", "").isnumeric(), self.versions)) 52 | 53 | 54 | class Packages(BaseModel): 55 | data: list[PackageToTest] = [] 56 | 57 | def append(self, item: PackageToTest) -> None: 58 | self.data.append(item) 59 | 60 | 61 | def test_package(pkg: PackageToTest) -> tuple[bool, dict]: 62 | 63 | log1 = start_capture_logs() 64 | retry = None 65 | use_version: Optional[str] = None 66 | try: 67 | use(pkg.name, modes=use.auto_install) 68 | except RuntimeWarning as e: 69 | if str(e).startswith("Please specify version and hash for auto-installation of"): 70 | retry = str(e.args[0]).strip().strip(".").splitlines()[-1] 71 | hashes = re.findall("hashes={([^}]+)}", str(e))[0] 72 | hashes = {_hash.strip("'") for _hash in hashes.split(", ")} 73 | print(str(e)) 74 | use_version = re.findall('version="(.*)", ', str(e))[0] 75 | else: 76 | exc_type, exc_value, _ = sys.exc_info() 77 | tb = traceback.format_exc() 78 | return ( 79 | False, 80 | { 81 | "name": pkg.name, 82 | "version": "None", 83 | "stars": pkg.stars, 84 | "err": { 85 | "type": str(exc_type), 86 | "value": str(exc_value), 87 | "traceback": tb.split("\n"), 88 | "logs": get_capture_logs(log1).split("\n"), 89 | }, 90 | }, 91 | ) 92 | 93 | except packaging.version.InvalidVersion: 94 | return ( 95 | False, 96 | { 97 | "name": pkg.name, 98 | "stars": pkg.stars, 99 | "err": {"type": "InvalidVersion", "value": pkg.versions, "picked": "None"}, 100 | }, 101 | ) 102 | 103 | except Exception as e: 104 | exc_type, exc_value, _ = sys.exc_info() 105 | tb = traceback.format_exc() 106 | return ( 107 | False, 108 | { 109 | "name": pkg.name, 110 | "version": use_version, 111 | "stars": pkg.stars, 112 | "err": { 113 | "type": str(exc_type), 114 | "value": str(exc_value), 115 | "traceback": tb.split("\n"), 116 | "logs": get_capture_logs(log1).split("\n"), 117 | }, 118 | }, 119 | ) 120 | 121 | else: 122 | get_capture_logs(log1) 123 | 124 | logs = start_capture_logs() 125 | try: 126 | module = use(pkg.name, version=use_version, modes=use.auto_install, hashes=hashes) 127 | assert module 128 | return ( 129 | True, 130 | { 131 | "name": pkg.name, 132 | "version": use_version, 133 | "stars": pkg.stars, 134 | "retry": retry, 135 | }, 136 | ) 137 | 138 | except Exception as e: 139 | exc_type, exc_value, _ = sys.exc_info() 140 | tb = traceback.format_exc() 141 | return ( 142 | False, 143 | { 144 | "name": pkg.name, 145 | "version": use_version, 146 | "stars": pkg.stars, 147 | "retry": retry, 148 | "err": { 149 | "type": str(exc_type), 150 | "value": str(exc_value), 151 | "traceback": tb.split("\n"), 152 | "logs": get_capture_logs(logs).split("\n"), 153 | }, 154 | }, 155 | ) 156 | 157 | 158 | if __name__ == "__main__": 159 | fail_dir = Path("results") / "fail" 160 | pass_dir = Path("results") / "pass" 161 | 162 | fail_dir.mkdir(parents=True, exist_ok=True) 163 | pass_dir.mkdir(parents=True, exist_ok=True) 164 | 165 | with open("pypi.json", "r") as f: 166 | packages = Packages(data=json.load(f)["data"]) 167 | 168 | index = sys.argv[1] 169 | if index.isdigit(): 170 | index = int(index) 171 | pkg = packages.data[index] 172 | else: 173 | pkg = next((item for item in packages.data if item.name == index), None) 174 | 175 | did_work, info = test_package(pkg) 176 | out_dir = pass_dir if did_work else fail_dir 177 | 178 | with open(out_dir / f"{pkg.name}.json", "w") as f: 179 | json.dump(info, f, indent=4, sort_keys=True) 180 | -------------------------------------------------------------------------------- /tests/integration/tmp.py: -------------------------------------------------------------------------------- 1 | import use 2 | 3 | mod = use( 4 | "pandas", 5 | version="1.3.3", 6 | modes=use.auto_install, 7 | hashes={ 8 | "68408a39a54ebadb9014ee5a4fae27b2fe524317bc80adf56c9ac59e8f8ea431", 9 | "e9bc59855598cb57f68fdabd4897d3ed2bc3a3b3bef7b868a0153c4cd03f3207", 10 | "3f5020613c1d8e304840c34aeb171377dc755521bf5e69804991030c2a48aec3", 11 | "c399200631db9bd9335d013ec7fce4edb98651035c249d532945c78ad453f23a", 12 | "f7d84f321674c2f0f31887ee6d5755c54ca1ea5e144d6d54b3bbf566dd9ea0cc", 13 | "86b16b1b920c4cb27fdd65a2c20258bcd9c794be491290660722bb0ea765054d", 14 | "a9f1b54d7efc9df05320b14a48fb18686f781aa66cc7b47bb62fabfc67a0985c", 15 | "272c8cb14aa9793eada6b1ebe81994616e647b5892a370c7135efb2924b701df", 16 | "7326b37de08d42dd3fff5b7ef7691d0fd0bf2428f4ba5a2bdc3b3247e9a52e4c", 17 | "3334a5a9eeaca953b9db1b2b165dcdc5180b5011f3bec3a57a3580c9c22eae68", 18 | "37d63e78e87eb3791da7be4100a65da0383670c2b59e493d9e73098d7a879226", 19 | "a800df4e101b721e94d04c355e611863cc31887f24c0b019572e26518cbbcab6", 20 | "4def2ef2fb7fcd62f2aa51bacb817ee9029e5c8efe42fe527ba21f6a3ddf1a9f", 21 | "45649503e167d45360aa7c52f18d1591a6d5c70d2f3a26bc90a3297a30ce9a66", 22 | "e574c2637c9d27f322e911650b36e858c885702c5996eda8a5a60e35e6648cf2", 23 | "ed2f29b4da6f6ae7c68f4b3708d9d9e59fa89b2f9e87c2b64ce055cbd39f729e", 24 | "7557b39c8e86eb0543a17a002ac1ea0f38911c3c17095bc9350d0a65b32d801c", 25 | "629138b7cf81a2e55aa29ce7b04c1cece20485271d1f6c469c6a0c03857db6a4", 26 | "53e2fb11f86f6253bb1df26e3aeab3bf2e000aaa32a953ec394571bec5dc6fd6", 27 | "ebbed7312547a924df0cbe133ff1250eeb94cdff3c09a794dc991c5621c8c735", 28 | "49fd2889d8116d7acef0709e4c82b8560a8b22b0f77471391d12c27596e90267", 29 | }, 30 | ) 31 | print(mod.DataFrame) 32 | -------------------------------------------------------------------------------- /tests/simple_funcs.py: -------------------------------------------------------------------------------- 1 | __package__ = "tests" 2 | 3 | def two(): 4 | return 2 5 | 6 | 7 | class Two: 8 | def __call__(self): 9 | return 2 10 | 11 | 12 | def three(): 13 | return 3 14 | 15 | 16 | class Three: 17 | def three(self): 18 | return 3 19 | -------------------------------------------------------------------------------- /tests/tdd_test.py: -------------------------------------------------------------------------------- 1 | """Here live all the tests that are expected to fail because their functionality is not implemented yet. 2 | Test-Driven Development is done in the following order: 3 | 1. Create a test that fails. 4 | 2. Write the code that makes the test pass. 5 | 3. Check how long the test took to run. 6 | 4. If it took longer than 1 second, move it to integration tests. Otherwise, move it to unit tests. 7 | """ 8 | 9 | import io 10 | import os 11 | import sys 12 | from contextlib import AbstractContextManager, closing, redirect_stdout 13 | from unittest.mock import patch 14 | 15 | from hypothesis import strategies as st 16 | from pytest import fixture, mark, raises, skip 17 | 18 | is_win = sys.platform.startswith("win") 19 | 20 | __package__ = "tests" 21 | import logging 22 | 23 | from use import auto_install, fatal_exceptions, no_cleanup, use 24 | from use.aspectizing import _unwrap, _wrap 25 | 26 | log = logging.getLogger(".".join((__package__, __name__))) 27 | log.setLevel(logging.DEBUG if "DEBUG" in os.environ else logging.NOTSET) 28 | 29 | use.config.testing = True 30 | 31 | 32 | @fixture() 33 | def reuse(): 34 | """ 35 | Return the `use` module in a clean state for "reuse." 36 | 37 | NOTE: making a completely new one each time would take 38 | ages, due to expensive _registry setup, re-downloading 39 | venv packages, etc., so if we are careful to reset any 40 | additional state changes on a case-by-case basis, 41 | this approach is more efficient and is the clear winner. 42 | """ 43 | use._using.clear() 44 | use.main._reloaders.clear() 45 | return use 46 | 47 | 48 | def test_test(reuse): 49 | assert reuse 50 | 51 | 52 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal, getcontext 2 | 3 | def compute(n): 4 | getcontext().prec = n 5 | res = Decimal(0) 6 | for i in range(n): 7 | a = Decimal(1)/(16**i) 8 | b = Decimal(4)/(8*i+1) 9 | c = Decimal(2)/(8*i+4) 10 | d = Decimal(1)/(8*i+5) 11 | e = Decimal(1)/(8*i+6) 12 | r = a*(b-c-d-e) 13 | res += r 14 | return res 15 | 16 | from time import perf_counter 17 | 18 | for x in range(1,10000, 100): 19 | before = perf_counter() 20 | compute(x) 21 | after = perf_counter() 22 | print(after-before) 23 | --------------------------------------------------------------------------------