Skip to content

feat(pre-commit): add internal-import alias check with auto-fix#49

Merged
dengqiaoyu merged 2 commits into
apple:mainfrom
dengqiaoyu:u/qiaoyu_deng/private-import-check
Jul 23, 2026
Merged

feat(pre-commit): add internal-import alias check with auto-fix#49
dengqiaoyu merged 2 commits into
apple:mainfrom
dengqiaoyu:u/qiaoyu_deng/private-import-check

Conversation

@dengqiaoyu

Copy link
Copy Markdown
Contributor
  • Add an AST-based hook enforcing code style guide §3.3: public modules must _-alias symbols imported from private modules; private modules must not
  • Auto-fix offending import statements and rename in-file references, refusing the fix only when the bound name is shadowed
  • Register the hook in .pre-commit-config.yaml and document it under §3.3 "Enforcement"
  • Apply across src/coreai_opt/, normalizing 27 modules with missing or redundant _ aliases

@dengqiaoyu
dengqiaoyu requested review from crowbat and guru-desh July 20, 2026 19:16
@dengqiaoyu dengqiaoyu added documentation Improvements or additions to documentation enhancement New feature or request labels Jul 20, 2026
yield from ast.walk(stmt)


def _locally_binds_name(node: ast.AST, name: str) -> bool:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 comments on this function:

  1. It looks like it misses out on the case where t in node.targets itself is a tuple/list and when node is a comprehension. Updating the checks to use _is_named_target could dedupe some logic and fix these issues at the same time:
def _locally_binds_name(node: ast.AST, name: str) -> bool:
    if isinstance(node, ast.Assign):
        return any(_is_named_target(t, name) for t in node.targets)
    if isinstance(node, (ast.AnnAssign, ast.AugAssign, ast.NamedExpr, ast.For,
ast.AsyncFor)):
        return _is_named_target(node.target, name)
    if isinstance(node, (ast.With, ast.AsyncWith)):
        return any(
            item.optional_vars is not None and
_is_named_target(item.optional_vars, name)
            for item in node.items
        )
    if isinstance(node, ast.comprehension):
        return _is_named_target(node.target, name)
    return False
  1. This function and _binds_assignment_target share very similar logic, could they be combined into one?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like it misses out on the case

Good catch. I think I was missing a few cases here:

a, helper = f()
for a, helper in items
with cm as (a, helper)
[y for helper in items]

This function and _binds_assignment_target share very similar logic, could they be combined into one?

Before your previous comment, these two had one difference. After adding the cases above, they can now be merged into one. I'll make that change.


def _node_span(node: _Positioned, line_starts: list[int]) -> tuple[int, int]:
"""Convert an AST node's (lineno, col_offset, end_lineno, end_col_offset) to byte offsets."""
start = line_starts[node.lineno - 1] + node.col_offset

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somewhat a corner case, but if the code has some character that is non-ASCII that uses more than one byte, the logic here can result in a malformed update:

# Before the fix
from temp_pkg._priv import helper

def use():
    café = ...
    return café + helper()

# After the fix
from temp_pkg._priv import helper as _helper
  
def use():
    café = ...
    return café + h_helper)

@dengqiaoyu dengqiaoyu Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I doubt people will use non-ASCII characters when writing code, but we should still handle this case. Will change it.

"""
violations: list[Violation] = []

for node in ast.walk(tree):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Should we also check for and flag plain imports of private packages? Like import pkg._private
  2. Looks like we are missing checks for relative imports (from ._priv import x)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added!

"""
original = alias.name
current_alias = alias.asname
if original.startswith("_") or current_alias not in (None, original, "_" + original):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another corner case, this currently does not handle the case if someone names things with a custom alias (from a._b import c as d)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added!

)


def _statement_edits(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an import line has a comment at the end, looks like the comment would be removed as part of the rename

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

Comment thread .pre-commit-config.yaml Outdated
symbols imported from private modules with a `_` prefix; private
modules must not. Auto-fixes the import statement and renames
references in the same file.
entry: python scripts/pre_commit/check_internal_import_aliases.py --fix

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general it seems like there are a lot of potential corner cases associated with automatically fixing the code. What do you think about making --fix opt-in instead where the user manually runs it if they want to have things be automatically fixed?

We could have the pre-commit hook only do the detection part of it to fail the commit if it finds any transgressions. I'm thinking that if any automatic fixes are made which break the code, other pre-commit hooks may also have run (linting, line too long, etc.) which would conflate the changes if the user needs to revert the code back to before this hook took action.

@dengqiaoyu dengqiaoyu Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about making --fix opt-in

Agreed. There are many corner cases that this code cannot comprehensively cover, it's essentially acting as an AST parser, and I don't want it to be too complicated .

I'll make the pre-commit hook a warning instead of an error. This way, it won't fail the commit, but it will generate error information with correction suggestions. I'll also add a --fix flag to let users fix everything in one go.

Thanks for the suggestion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's what I came up with:

For the two attached files, _alias_demo.py and alias_demo.py, here's what we'd get if we committed them directly with the pre-commit hook added:

Check `_`-prefix aliasing of internal imports..............................Failed
- hook id: check-internal-import-aliases
- exit code: 1

src/coreai_opt/_alias_demo.py:14: Unnecessary `_` alias for `CompressionType` imported from private module `coreai_opt._utils.metadata_utils` into a private module.; Unnecessary `_` alias for `MILCompressionMetadata` imported from private module `coreai_opt._utils.metadata_utils` into a private module.
src/coreai_opt/_alias_demo.py:18: Unnecessary `_` alias for `move_model_to_eval` imported from private module `coreai_opt._utils.torch_utils` into a private module.
src/coreai_opt/_alias_demo.py:19: Unnecessary `_` alias for `version_ge` imported from private module `coreai_opt._utils.version_utils` into a private module.
src/coreai_opt/_alias_demo.py:21: Unnecessary `_` alias for `PartialConstructor` imported from private module `._utils.spec_utils` into a private module.
src/coreai_opt/alias_demo.py:16: Missing `_` alias for private module `coreai_opt._utils.registry_utils` bound as `registry` in a public module. Use `import coreai_opt._utils.registry_utils as _registry`.
src/coreai_opt/alias_demo.py:17: Missing `_` alias for `CompressionType` imported from private module `coreai_opt._utils.metadata_utils` into a public module. Use `CompressionType as _CompressionType`.; Missing `_` alias for `MILCompressionMetadata` imported from private module `coreai_opt._utils.metadata_utils` into a public module. Use `MILCompressionMetadata as _MILCompressionMetadata`.
src/coreai_opt/alias_demo.py:21: Missing `_` alias for `PartialConstructor` imported from private module `coreai_opt._utils.spec_utils` into a public module. Use `PartialConstructor as _PC`.
src/coreai_opt/alias_demo.py:22: Missing `_` alias for `move_model_to_eval` imported from private module `coreai_opt._utils.torch_utils` into a public module. Use `move_model_to_eval as _move_model_to_eval`. (needs a manual fix: bound name is shadowed)
src/coreai_opt/alias_demo.py:23: Missing `_` alias for `version_ge` imported from private module `coreai_opt._utils.version_utils` into a public module. Use `version_ge as _version_ge`.
src/coreai_opt/alias_demo.py:25: Missing `_` alias for `lazy_import_coreai_torch` imported from private module `._utils.import_utils` into a public module. Use `lazy_import_coreai_torch as _lazy_import_coreai_torch`.

10 issue(s) found. Fix them before committing — run `python scripts/pre_commit/check_internal_import_aliases.py --fix` to apply the fixable ones automatically.

And here's the output from running the auto-fix:

$ python scripts/pre_commit/check_internal_import_aliases.py --fix

src/coreai_opt/_alias_demo.py:14: FIXED — Unnecessary `_` alias for `CompressionType` imported from private module `coreai_opt._utils.metadata_utils` into a private module.; Unnecessary `_` alias for `MILCompressionMetadata` imported from private module `coreai_opt._utils.metadata_utils` into a private module.
src/coreai_opt/_alias_demo.py:18: FIXED — Unnecessary `_` alias for `move_model_to_eval` imported from private module `coreai_opt._utils.torch_utils` into a private module.
src/coreai_opt/_alias_demo.py:19: FIXED — Unnecessary `_` alias for `version_ge` imported from private module `coreai_opt._utils.version_utils` into a private module.
src/coreai_opt/_alias_demo.py:21: FIXED — Unnecessary `_` alias for `PartialConstructor` imported from private module `._utils.spec_utils` into a private module.
src/coreai_opt/alias_demo.py:16: FIXED — Missing `_` alias for private module `coreai_opt._utils.registry_utils` bound as `registry` in a public module. Use `import coreai_opt._utils.registry_utils as _registry`.
src/coreai_opt/alias_demo.py:17: FIXED — Missing `_` alias for `CompressionType` imported from private module `coreai_opt._utils.metadata_utils` into a public module. Use `CompressionType as _CompressionType`.; Missing `_` alias for `MILCompressionMetadata` imported from private module `coreai_opt._utils.metadata_utils` into a public module. Use `MILCompressionMetadata as _MILCompressionMetadata`.
src/coreai_opt/alias_demo.py:21: FIXED — Missing `_` alias for `PartialConstructor` imported from private module `coreai_opt._utils.spec_utils` into a public module. Use `PartialConstructor as _PC`.
src/coreai_opt/alias_demo.py:23: FIXED — Missing `_` alias for `version_ge` imported from private module `coreai_opt._utils.version_utils` into a public module. Use `version_ge as _version_ge`.
src/coreai_opt/alias_demo.py:25: FIXED — Missing `_` alias for `lazy_import_coreai_torch` imported from private module `._utils.import_utils` into a public module. Use `lazy_import_coreai_torch as _lazy_import_coreai_torch`.
src/coreai_opt/alias_demo.py:22: Missing `_` alias for `move_model_to_eval` imported from private module `coreai_opt._utils.torch_utils` into a public module. Use `move_model_to_eval as _move_model_to_eval`. (needs a manual fix: bound name is shadowed)

1 issue(s) could not be auto-fixed; resolve them by hand.

_alias_demo.py
alias_demo.py

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, I do like keeping it as an error instead of just a warning

- Add an AST-based hook enforcing code style guide §3.3: public modules must alias symbols imported from a private module with a `_` prefix; private modules must not
- On commit it fails and reports each violation with a suggested fix, but does not edit files — run the script with `--fix` to apply fixes manually (kept opt-in so its rewrites don't get entangled with the formatter/linter hooks)
- Detect absolute, relative, and aliased plain imports, plus custom aliases; preserve comments and handle multibyte source via byte offsets; skip cases that are unsafe to rename (shadowed or rebound names)
- Add a (before, after) test suite under `tests/devtools/` covering every case
- Add the required `_` alias to private-module imports in public modules, drop the redundant `_` alias in private modules, and rename references to match, per code style guide §3.3
- Generated with `python scripts/pre_commit/check_internal_import_aliases.py --fix`
- Kept as a separate commit from the hook so a future rebase conflict here can be resolved by reverting `src/` and re-running `--fix`, instead of hand-merging auto-generated edits
@dengqiaoyu
dengqiaoyu force-pushed the u/qiaoyu_deng/private-import-check branch from 23e77b7 to 7c01c37 Compare July 23, 2026 00:22

@crowbat crowbat left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, thanks for the fixes!

@dengqiaoyu
dengqiaoyu merged commit 2d92c5c into apple:main Jul 23, 2026
11 checks passed
@dengqiaoyu
dengqiaoyu deleted the u/qiaoyu_deng/private-import-check branch July 23, 2026 17:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants