├── CHANGELOG.md ├── docs ├── changelog.md ├── contributing.md └── index.md ├── .python-version ├── src └── canonical_imports │ ├── __init__.py │ └── __main__.py ├── tests ├── test_imports.py └── __init__.py ├── LICENSE.txt ├── mkdocs.yml ├── CONTRIBUTING.md ├── .gitignore ├── .pre-commit-config.yaml ├── pyproject.toml └── README.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | --8<-- "CHANGELOG.md" 2 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | --8<-- "CONTRIBUTING.md" 2 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 2 | 3.9 3 | 3.10 4 | 3.11 5 | 3.12 6 | -------------------------------------------------------------------------------- /src/canonical_imports/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.0.1" 2 | -------------------------------------------------------------------------------- /tests/test_imports.py: -------------------------------------------------------------------------------- 1 | def test_something(): 2 | pass 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023-present Frank Hoffmann <15r10nk@polarbit.de> 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | --8<-- "README.md:Header" 3 | 4 | 5 | # Welcome to canonical-imports 6 | 7 | 8 | This project and all the here described features are currently only available for insiders. 9 | -------------------------------------------------------------------------------- /src/canonical_imports/__main__.py: -------------------------------------------------------------------------------- 1 | def main(): 2 | print( 3 | """\ 4 | This project is currently only available for my insiders. 5 | It will become available for every one when I reached my first goal of 10 sponsors. 6 | 7 | https://github.com/sponsors/15r10nk 8 | """ 9 | ) 10 | 11 | 12 | if __name__ == "__main__": 13 | main() 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Frank Hoffmann <15r10nk@polarbit.de> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: canonical-imports 2 | site_url: https://15r10nk.github.io/canonical-imports/ 3 | repo_url: https://github.com/15r10nk/canonical-imports/ 4 | edit_uri: edit/main/docs 5 | 6 | theme: 7 | name: material 8 | features: 9 | - toc.follow 10 | palette: 11 | primary: teal 12 | 13 | watch: 14 | - CONTRIBUTING.md 15 | - CHANGELOG.md 16 | - README.md 17 | 18 | nav: 19 | - Introduction: index.md 20 | - Contributing: contributing.md 21 | - Changelog: changelog.md 22 | 23 | 24 | markdown_extensions: 25 | - toc: 26 | permalink: true 27 | - admonition 28 | - pymdownx.highlight: 29 | anchor_linenums: true 30 | - pymdownx.inlinehilite 31 | - pymdownx.snippets: 32 | check_paths: true 33 | - pymdownx.superfences 34 | - admonition 35 | - pymdownx.details 36 | - pymdownx.superfences 37 | - pymdownx.tabbed: 38 | alternate_style: true 39 | 40 | plugins: 41 | - mkdocstrings: 42 | handlers: 43 | python: 44 | options: 45 | show_root_heading: true 46 | show_source: false 47 | - social 48 | - search 49 | 50 | extra: 51 | social: 52 | - icon: fontawesome/brands/twitter 53 | link: https://x.com/15r10nk 54 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Contributions are welcome. 3 | Please create an issue before writing a pull request so we can discuss what needs to be changed. 4 | 5 | # Testing 6 | The code can be tested with [hatch](https://hatch.pypa.io/latest/) 7 | 8 | * `hatch run cov:test` can be used to test all supported python versions and to check for coverage. 9 | * `hatch run +py=3.10 all:test -- --sw` runs pytest for python 3.10 with the `--sw` argument. 10 | 11 | 12 | # Coverage 13 | This project has a hard coverage requirement of 100%. 14 | The goal here is to find different edge cases which might have bugs. 15 | 16 | However, it is possible to exclude some code from the coverage. 17 | 18 | Code can be marked with `pragma: no cover`, if it can not be tested for some reason. 19 | This makes it easy to spot uncovered code in the source. 20 | 21 | Impossible conditions can be handled with `assert False`. 22 | ``` python 23 | if some_condition: 24 | ... 25 | if some_other_codition: 26 | ... 27 | else: 28 | assert False, "unreachable because ..." 29 | ``` 30 | This serves also as an additional check during runtime. 31 | 32 | 33 | # Commits 34 | Please use [pre-commit](https://pre-commit.com/) for your commits. 35 | -------------------------------------------------------------------------------- /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | .pytest_cache 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask instance folder 58 | instance/ 59 | 60 | # Sphinx documentation 61 | docs/_build/ 62 | 63 | # MkDocs documentation 64 | /site/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | .mutmut-cache 75 | html 76 | .coverage 77 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autofix_pr: false 3 | autoupdate_commit_msg: 'style: pre-commit autoupdate' 4 | autofix_commit_msg: 'style: pre-commit fixed styling' 5 | autoupdate_schedule: monthly 6 | 7 | repos: 8 | - hooks: 9 | - id: check-yaml 10 | - id: check-ast 11 | - id: check-docstring-first 12 | - id: check-merge-conflict 13 | - id: trailing-whitespace 14 | - id: mixed-line-ending 15 | - id: fix-byte-order-marker 16 | - id: check-case-conflict 17 | - id: check-json 18 | - id: end-of-file-fixer 19 | repo: https://github.com/pre-commit/pre-commit-hooks 20 | rev: v4.5.0 21 | - hooks: 22 | - args: 23 | - --in-place 24 | - --expand-star-imports 25 | - --remove-all-unused-imports 26 | - --ignore-init-module-imports 27 | id: autoflake 28 | repo: https://github.com/myint/autoflake 29 | rev: v2.3.1 30 | 31 | - repo: https://github.com/asottile/setup-cfg-fmt 32 | rev: v2.5.0 33 | hooks: 34 | - id: setup-cfg-fmt 35 | 36 | - repo: https://github.com/asottile/reorder-python-imports 37 | rev: v3.12.0 38 | hooks: 39 | - args: 40 | - --py38-plus 41 | id: reorder-python-imports 42 | - hooks: 43 | - args: 44 | - --py38-plus 45 | id: pyupgrade 46 | repo: https://github.com/asottile/pyupgrade 47 | rev: v3.15.2 48 | - hooks: 49 | - id: black 50 | repo: https://github.com/psf/black 51 | rev: 24.3.0 52 | - hooks: 53 | - id: blacken-docs 54 | repo: https://github.com/asottile/blacken-docs 55 | rev: 1.16.0 56 | 57 | - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks 58 | rev: v2.13.0 59 | hooks: 60 | - id: pretty-format-yaml 61 | args: [--autofix, --indent, '2'] 62 | - id: pretty-format-toml 63 | exclude: ^poetry.lock$ 64 | args: [--autofix] 65 | 66 | - hooks: 67 | - id: blackdoc 68 | repo: https://github.com/keewis/blackdoc 69 | rev: v0.3.9 70 | - hooks: 71 | - id: commitizen 72 | stages: 73 | - commit-msg 74 | repo: https://github.com/commitizen-tools/commitizen 75 | rev: v3.21.3 76 | 77 | 78 | - repo: https://github.com/PyCQA/docformatter 79 | rev: v1.7.5 80 | hooks: 81 | - id: docformatter 82 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = ["hatchling"] 4 | 5 | [project] 6 | authors = [ 7 | {name = "Frank Hoffmann", email = "15r10nk@polarbit.de"} 8 | ] 9 | classifiers = [ 10 | "Development Status :: 4 - Beta", 11 | "Programming Language :: Python", 12 | "Programming Language :: Python :: 3.8", 13 | "Programming Language :: Python :: 3.9", 14 | "Programming Language :: Python :: 3.10", 15 | "Programming Language :: Python :: 3.11", 16 | "Programming Language :: Python :: 3.12", 17 | "Programming Language :: Python :: Implementation :: CPython", 18 | "Programming Language :: Python :: Implementation :: PyPy" 19 | ] 20 | dependencies = [] 21 | description = 'directly import everything from where it is defined' 22 | dynamic = ["version"] 23 | keywords = [] 24 | license = "MIT" 25 | name = "canonical-imports" 26 | readme = "README.md" 27 | requires-python = ">=3.8" 28 | 29 | [project.scripts] 30 | canonical-imports = "canonical_imports.__main__:main" 31 | 32 | [project.urls] 33 | Documentation = "https://github.com/unknown/canonical-imports#readme" 34 | Issues = "https://github.com/unknown/canonical-imports/issues" 35 | Source = "https://github.com/unknown/canonical-imports" 36 | 37 | [tool.black] 38 | target-version = ["py38"] 39 | 40 | [tool.commitizen] 41 | changelog_incremental = true 42 | major_version_zero = true 43 | tag_format = "v$major.$minor.$patch$prerelease" 44 | update_changelog_on_bump = true 45 | version_files = [ 46 | "inline_snapshot/__init__.py:version" 47 | ] 48 | version_provider = "poetry" 49 | 50 | [tool.coverage.paths] 51 | canonical_imports = ["src/canonical_imports", "*/canonical-imports/src/canonical_imports"] 52 | tests = ["tests", "*/canonical-imports/tests"] 53 | 54 | [tool.coverage.report] 55 | exclude_lines = ["pragma: no cover", "assert False", "raise NotImplemented", "\\.\\.\\."] 56 | 57 | [tool.coverage.run] 58 | branch = true 59 | data_file = "$TOP/.coverage" 60 | omit = [ 61 | "src/canonical_imports/__about__.py" 62 | ] 63 | parallel = true 64 | source_pkgs = ["canonical_imports", "tests"] 65 | 66 | [[tool.hatch.envs.all.matrix]] 67 | python = ["3.8", "3.9", "3.10", "3.11", "3.12"] 68 | 69 | [tool.hatch.envs.cov] 70 | dependencies = ["coverage[toml]>=6.5"] 71 | detached = true 72 | scripts.test = [ 73 | "- coverage erase", 74 | "hatch run all:test-cov -- {args}", 75 | "- coverage combine", 76 | "coverage report" 77 | ] 78 | 79 | [tool.hatch.envs.default] 80 | dependencies = [ 81 | "coverage[toml]>=6.5", 82 | "pytest" 83 | ] 84 | 85 | [tool.hatch.envs.docs] 86 | dependencies = [ 87 | "mkdocs>=1.4.2", 88 | "mkdocs-material[imaging]>=8.5.10", 89 | "mkdocstrings[python-legacy]>=0.19.0" 90 | ] 91 | env-vars.TOP = "{root}" 92 | scripts.serve = ["mkdocs serve {args}"] 93 | scripts.test = "pytest {args:tests}" 94 | scripts.test-cov = "coverage run -m pytest {args:tests}" 95 | 96 | [tool.hatch.version] 97 | path = "src/canonical_imports/__init__.py" 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | [![Docs](https://img.shields.io/badge/docs-mkdocs-green)](https://15r10nk.github.io/canonical-imports/) 5 | [![pypi version](https://img.shields.io/pypi/v/canonical-imports.svg)](https://pypi.org/project/canonical-imports/) 6 | ![Python Versions](https://img.shields.io/pypi/pyversions/canonical-imports) 7 | ![PyPI - Downloads](https://img.shields.io/pypi/dw/canonical-imports) 8 | [![coverage](https://img.shields.io/badge/coverage-100%25-blue)](https://15r10nk.github.io/canonical-imports/contributing/#coverage) 9 | [![GitHub Sponsors](https://img.shields.io/github/sponsors/15r10nk)](https://github.com/sponsors/15r10nk) 10 | 11 | 12 | 13 | Managing imports is difficult when the project grows in size. Functions and classes gets moved or renamed. 14 | `canonical-imports` follows your imports and finds out where the things you are importing are actually defined. 15 | It can change your imports which makes your code cleaner and maybe even faster. 16 | 17 | ## Installation 18 | 19 | 20 | This project is currently only available for insiders, which mean that you can get access to it if you [sponsor](https://github.com/sponsors/15r10nk) me. 21 | You should then have access to [this repository](https://github.com/15r10nk-insiders/canonical-imports). 22 | 23 | You can install it with pip and the github url. 24 | 25 | ``` bash 26 | pip install git+ssh://git@github.com/15r10nk-insiders/canonical-imports.git@insider 27 | ``` 28 | 29 | ## Key Features 30 | 31 | - follow imports to their definition and replace them. 32 | - options to prevent the following of some types of imports (from public to private modules). 33 | 34 | 35 | I will show you what it does with the following example: 36 | 37 | ``` python 38 | # m/a.py 39 | from ._core import helper 40 | 41 | # m/_core.py 42 | from ._utils import helper 43 | 44 | # m/_utils.py 45 | 46 | 47 | def helper(): 48 | print("some help") 49 | ``` 50 | 51 | `helper` was moved from `_core` to `_utils` 52 | 53 | ``` bash 54 | canonical-imports -w m/a.py 55 | ``` 56 | 57 | changes `m/a.py` to: 58 | 59 | ``` python 60 | # m/a.py 61 | from ._utils import helper 62 | ``` 63 | 64 | 65 | ## Usage 66 | 67 | You can use `canonical-imports` from the command line to fix some files. 68 | 69 | ```bash 70 | canonical-imports my_package/something.py 71 | ``` 72 | 73 | Use `canonical-imports --help` for more options. 74 | 75 | ### Options 76 | 77 | canonical-imports follows all imports by default. `--no` can be used to prevent certain types of import changes. 78 | - `--no public-private` prevents changing public imports into private imports like in the following: 79 | ``` diff 80 | -from package.module import Thing 81 | +from package.module._submodule import Thing 82 | ``` 83 | - `--no into-init` prevents following imports into `__init__.py` files. 84 | Example: 85 | ``` python 86 | # m/__init__.py 87 | ... 88 | 89 | # m/a.py 90 | from .b import f # <-- change to: from .q import f 91 | 92 | # m/b.py 93 | from .q import f # prevent changing to: from .q.c import f 94 | 95 | # m/q/__init__.py 96 | from .c import f 97 | 98 | 99 | # m/q/c.py 100 | def f(): 101 | pass 102 | ``` 103 | This rule does nothing if the import chain leaves the package `m.q` again (if `f` would be defined another package `m.x` for example). 104 | This option might be useful if you do not use private module paths (with leading `_`). 105 | 106 | 107 | 108 | ## Issues 109 | 110 | If you encounter any problems, please [report an issue](https://github.com/15r10nk/canonical-imports/issues) along with a detailed description. 111 | 112 | 113 | ## License 114 | 115 | Distributed under the terms of the [MIT](http://opensource.org/licenses/MIT) license, "canonical-imports" is free and open source software. 116 | --------------------------------------------------------------------------------