pyflyby 1.10.1__tar.gz → 1.10.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyflyby might be problematic. Click here for more details.

Files changed (140) hide show
  1. {pyflyby-1.10.1 → pyflyby-1.10.2}/.github/workflows/docs.yml +1 -1
  2. {pyflyby-1.10.1 → pyflyby-1.10.2}/.github/workflows/lint.yml +1 -1
  3. {pyflyby-1.10.1 → pyflyby-1.10.2}/.github/workflows/test.yml +1 -1
  4. {pyflyby-1.10.1 → pyflyby-1.10.2}/PKG-INFO +23 -6
  5. {pyflyby-1.10.1 → pyflyby-1.10.2}/README.rst +22 -5
  6. {pyflyby-1.10.1 → pyflyby-1.10.2}/bin/tidy-imports +8 -1
  7. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_cmdline.py +62 -19
  8. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_modules.py +7 -1
  9. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_version.py +1 -1
  10. {pyflyby-1.10.1 → pyflyby-1.10.2}/meson.build +1 -1
  11. {pyflyby-1.10.1 → pyflyby-1.10.2}/src/_fast_iter_modules.cpp +26 -15
  12. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_cmdline.py +261 -1
  13. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_modules.py +20 -1
  14. {pyflyby-1.10.1 → pyflyby-1.10.2}/.github/dependabot.yml +0 -0
  15. {pyflyby-1.10.1 → pyflyby-1.10.2}/.gitignore +0 -0
  16. {pyflyby-1.10.1 → pyflyby-1.10.2}/.pyflyby +0 -0
  17. {pyflyby-1.10.1 → pyflyby-1.10.2}/CONTRIBUTING.md +0 -0
  18. {pyflyby-1.10.1 → pyflyby-1.10.2}/LICENSE.txt +0 -0
  19. {pyflyby-1.10.1 → pyflyby-1.10.2}/MANIFEST.in +0 -0
  20. {pyflyby-1.10.1 → pyflyby-1.10.2}/bin/autoipython +0 -0
  21. {pyflyby-1.10.1 → pyflyby-1.10.2}/bin/autopython +0 -0
  22. {pyflyby-1.10.1 → pyflyby-1.10.2}/bin/collect-exports +0 -0
  23. {pyflyby-1.10.1 → pyflyby-1.10.2}/bin/collect-imports +0 -0
  24. {pyflyby-1.10.1 → pyflyby-1.10.2}/bin/create-imports +0 -0
  25. {pyflyby-1.10.1 → pyflyby-1.10.2}/bin/find-import +0 -0
  26. {pyflyby-1.10.1 → pyflyby-1.10.2}/bin/list-bad-xrefs +0 -0
  27. {pyflyby-1.10.1 → pyflyby-1.10.2}/bin/prune-broken-imports +0 -0
  28. {pyflyby-1.10.1 → pyflyby-1.10.2}/bin/py +0 -0
  29. {pyflyby-1.10.1 → pyflyby-1.10.2}/bin/pyflyby-diff +0 -0
  30. {pyflyby-1.10.1 → pyflyby-1.10.2}/bin/reformat-imports +0 -0
  31. {pyflyby-1.10.1 → pyflyby-1.10.2}/bin/replace-star-imports +0 -0
  32. {pyflyby-1.10.1 → pyflyby-1.10.2}/bin/saveframe +0 -0
  33. {pyflyby-1.10.1 → pyflyby-1.10.2}/bin/transform-imports +0 -0
  34. {pyflyby-1.10.1 → pyflyby-1.10.2}/codecov.yml +0 -0
  35. {pyflyby-1.10.1 → pyflyby-1.10.2}/conftest.py +0 -0
  36. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/LICENSE.txt +0 -0
  37. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/Makefile +0 -0
  38. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/TODO.txt +0 -0
  39. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/__init__.py +0 -0
  40. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/api.rst +0 -0
  41. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/autoimp.rst +0 -0
  42. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/cmdline.rst +0 -0
  43. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/comms.rst +0 -0
  44. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/dbg.rst +0 -0
  45. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/file.rst +0 -0
  46. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/flags.rst +0 -0
  47. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/format.rst +0 -0
  48. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/idents.rst +0 -0
  49. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/importclns.rst +0 -0
  50. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/importdb.rst +0 -0
  51. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/imports2s.rst +0 -0
  52. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/importstmt.rst +0 -0
  53. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/interactive.rst +0 -0
  54. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/livepatch.rst +0 -0
  55. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/log.rst +0 -0
  56. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/modules.rst +0 -0
  57. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/parse.rst +0 -0
  58. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/py.rst +0 -0
  59. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/api/util.rst +0 -0
  60. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/cli/autoipython.rst +0 -0
  61. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/cli/cli.rst +0 -0
  62. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/cli/collect_exports.rst +0 -0
  63. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/cli/collect_imports.rst +0 -0
  64. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/cli/find_import.rst +0 -0
  65. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/cli/prune_broken_imports.rst +0 -0
  66. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/cli/py.rst +0 -0
  67. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/cli/pyflyby_diff.rst +0 -0
  68. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/cli/reformat_imports.rst +0 -0
  69. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/cli/replace_star_imports.rst +0 -0
  70. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/cli/tidy_imports.rst +0 -0
  71. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/cli/transform_imports.rst +0 -0
  72. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/conf.py +0 -0
  73. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/index.rst +0 -0
  74. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/make.bat +0 -0
  75. {pyflyby-1.10.1 → pyflyby-1.10.2}/doc/testing.txt +0 -0
  76. {pyflyby-1.10.1 → pyflyby-1.10.2}/etc/pyflyby/.gitignore +0 -0
  77. {pyflyby-1.10.1 → pyflyby-1.10.2}/etc/pyflyby/canonical.py +0 -0
  78. {pyflyby-1.10.1 → pyflyby-1.10.2}/etc/pyflyby/common.py +0 -0
  79. {pyflyby-1.10.1 → pyflyby-1.10.2}/etc/pyflyby/forget.py +0 -0
  80. {pyflyby-1.10.1 → pyflyby-1.10.2}/etc/pyflyby/mandatory.py +0 -0
  81. {pyflyby-1.10.1 → pyflyby-1.10.2}/etc/pyflyby/numpy.py +0 -0
  82. {pyflyby-1.10.1 → pyflyby-1.10.2}/etc/pyflyby/std.py +0 -0
  83. {pyflyby-1.10.1 → pyflyby-1.10.2}/github_deploy_key_deshaw_pyflyby.enc +0 -0
  84. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/emacs/pyflyby.el +0 -0
  85. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/.gitignore +0 -0
  86. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/.gitignore +0 -0
  87. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/__init__.py +0 -0
  88. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/__main__.py +0 -0
  89. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_autoimp.py +0 -0
  90. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_comms.py +0 -0
  91. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_dbg.py +0 -0
  92. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_docxref.py +0 -0
  93. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_dynimp.py +0 -0
  94. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_file.py +0 -0
  95. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_flags.py +0 -0
  96. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_format.py +0 -0
  97. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_idents.py +0 -0
  98. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_import_sorting.py +0 -0
  99. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_importclns.py +0 -0
  100. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_importdb.py +0 -0
  101. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_imports2s.py +0 -0
  102. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_importstmt.py +0 -0
  103. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_interactive.py +0 -0
  104. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_livepatch.py +0 -0
  105. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_log.py +0 -0
  106. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_parse.py +0 -0
  107. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_py.py +0 -0
  108. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_saveframe.py +0 -0
  109. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_saveframe_reader.py +0 -0
  110. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/_util.py +0 -0
  111. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/autoimport.py +0 -0
  112. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/importdb.py +0 -0
  113. {pyflyby-1.10.1 → pyflyby-1.10.2}/lib/python/pyflyby/meson.build +0 -0
  114. {pyflyby-1.10.1 → pyflyby-1.10.2}/libexec/pyflyby/colordiff +0 -0
  115. {pyflyby-1.10.1 → pyflyby-1.10.2}/libexec/pyflyby/diff-colorize +0 -0
  116. {pyflyby-1.10.1 → pyflyby-1.10.2}/pyproject.toml +0 -0
  117. {pyflyby-1.10.1 → pyflyby-1.10.2}/pytest.ini +0 -0
  118. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/__init__.py +0 -0
  119. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_0testconfig.py +0 -0
  120. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_autoimp.py +0 -0
  121. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_docxref.py +0 -0
  122. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_file.py +0 -0
  123. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_flags.py +0 -0
  124. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_format.py +0 -0
  125. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_idents.py +0 -0
  126. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_importclns.py +0 -0
  127. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_importdb.py +0 -0
  128. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_imports2s.py +0 -0
  129. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_importstmt.py +0 -0
  130. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_interactive.py +0 -0
  131. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_jupyterlab_pyflyby.py +0 -0
  132. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_livepatch.py +0 -0
  133. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_parse.py +0 -0
  134. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_py.py +0 -0
  135. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_saveframe.py +0 -0
  136. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_saveframe_reader.py +0 -0
  137. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/test_util.py +0 -0
  138. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/tests_sorts.py +0 -0
  139. {pyflyby-1.10.1 → pyflyby-1.10.2}/tests/xrefs.py +0 -0
  140. {pyflyby-1.10.1 → pyflyby-1.10.2}/tox.ini +0 -0
@@ -9,7 +9,7 @@ jobs:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
11
  - uses: actions/checkout@v5
12
- - uses: actions/setup-python@v5
12
+ - uses: actions/setup-python@v6
13
13
  - name: Install dependencies
14
14
  run: |
15
15
  pip install -U pip
@@ -8,7 +8,7 @@ jobs:
8
8
  steps:
9
9
  - uses: actions/checkout@v5
10
10
  - name: Set up Python
11
- uses: actions/setup-python@v5
11
+ uses: actions/setup-python@v6
12
12
  with:
13
13
  python-version: 3.13
14
14
  - name: Install pyflyby
@@ -27,7 +27,7 @@ jobs:
27
27
  steps:
28
28
  - uses: actions/checkout@v5
29
29
  - name: Set up Python ${{ matrix.python-version }}
30
- uses: actions/setup-python@v5
30
+ uses: actions/setup-python@v6
31
31
  with:
32
32
  python-version: ${{ matrix.python-version }}
33
33
  - name: Install pyflyby
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyflyby
3
- Version: 1.10.1
3
+ Version: 1.10.2
4
4
  Summary: pyflyby - Python development productivity tools, in particular automatic import management
5
5
  Author-Email: Karl Chen <quarl@8166.clguba.z.quarl.org>
6
6
  License-Expression: MIT
@@ -210,6 +210,7 @@ For example:
210
210
 
211
211
  Replace /tmp/foo.py? [y/N]
212
212
 
213
+ To exclude a file, use `--exclude <pattern>`.
213
214
 
214
215
  Quick start: import libraries
215
216
  =============================
@@ -481,21 +482,37 @@ Per-Project configuration of tidy-imports
481
482
  =========================================
482
483
 
483
484
  You can configure Pyflyby on a per-repository basis by using the
484
- `[tool.pyflyby]` section of `pyproject.toml` files. Pyflyby will look in current
485
- working directory and all it's parent until it find a `pyproject.toml` file from
485
+ ``[tool.pyflyby]`` section of ``pyproject.toml`` files. Pyflyby will look in current
486
+ working directory and all it's parent until it find a ``pyproject.toml`` file from
486
487
  which it will load the defaults.
487
488
 
488
489
 
489
490
  Most of the long command line flags default values can be configured in this
490
- section. Simply use the long form option name by replacing dashes `-` by
491
- underscore `_`. For long option that have the form `--xxx` and `--no-xxx`, you
492
- can assign a boolean to `xxx`. For example::
491
+ section. Simply use the long form option name by replacing dashes ``-`` by
492
+ underscore ``_``. For long option that have the form ``--xxx`` and ``--no-xxx``, you
493
+ can assign a boolean to ``xxx``. For example::
494
+
495
+ .. code:: toml
493
496
 
494
497
  [tool.pyflyby]
495
498
  add_missing=true
496
499
  from_spaces=7
497
500
  remove_unused=false
498
501
 
502
+ To exclude files from ``tidy-imports``, add an exclusion pattern to
503
+ ``tool.pyflyby.tidy-imports.exclude``:
504
+
505
+ .. code:: toml
506
+
507
+ [tool.pyflyby.tidy-imports]
508
+ exclude = [
509
+ "foo.py",
510
+ "baz/*.py"
511
+ ]
512
+
513
+ Exclusions are assumed to be relative to the project root if a ``pyproject.toml`` exists, unless an
514
+ absolute path is specified. Consult the documentation for ``pathlib.Path.match`` for information about
515
+ valid exclusion patterns.
499
516
 
500
517
  Emacs support
501
518
  =============
@@ -167,6 +167,7 @@ For example:
167
167
 
168
168
  Replace /tmp/foo.py? [y/N]
169
169
 
170
+ To exclude a file, use `--exclude <pattern>`.
170
171
 
171
172
  Quick start: import libraries
172
173
  =============================
@@ -438,21 +439,37 @@ Per-Project configuration of tidy-imports
438
439
  =========================================
439
440
 
440
441
  You can configure Pyflyby on a per-repository basis by using the
441
- `[tool.pyflyby]` section of `pyproject.toml` files. Pyflyby will look in current
442
- working directory and all it's parent until it find a `pyproject.toml` file from
442
+ ``[tool.pyflyby]`` section of ``pyproject.toml`` files. Pyflyby will look in current
443
+ working directory and all it's parent until it find a ``pyproject.toml`` file from
443
444
  which it will load the defaults.
444
445
 
445
446
 
446
447
  Most of the long command line flags default values can be configured in this
447
- section. Simply use the long form option name by replacing dashes `-` by
448
- underscore `_`. For long option that have the form `--xxx` and `--no-xxx`, you
449
- can assign a boolean to `xxx`. For example::
448
+ section. Simply use the long form option name by replacing dashes ``-`` by
449
+ underscore ``_``. For long option that have the form ``--xxx`` and ``--no-xxx``, you
450
+ can assign a boolean to ``xxx``. For example::
451
+
452
+ .. code:: toml
450
453
 
451
454
  [tool.pyflyby]
452
455
  add_missing=true
453
456
  from_spaces=7
454
457
  remove_unused=false
455
458
 
459
+ To exclude files from ``tidy-imports``, add an exclusion pattern to
460
+ ``tool.pyflyby.tidy-imports.exclude``:
461
+
462
+ .. code:: toml
463
+
464
+ [tool.pyflyby.tidy-imports]
465
+ exclude = [
466
+ "foo.py",
467
+ "baz/*.py"
468
+ ]
469
+
470
+ Exclusions are assumed to be relative to the project root if a ``pyproject.toml`` exists, unless an
471
+ absolute path is specified. Consult the documentation for ``pathlib.Path.match`` for information about
472
+ valid exclusion patterns.
456
473
 
457
474
  Emacs support
458
475
  =============
@@ -85,6 +85,8 @@ def _addopts(parser):
85
85
  default=True, action='store_false',
86
86
  help=hfmt('''
87
87
  Don't canonicalize imports.'''))
88
+ parser.add_option('--exclude', type='string', dest='exclude',
89
+ action='append', help=hfmt('Files to exclude from formatting.'))
88
90
 
89
91
 
90
92
  def transform_callback(option, opt_str, value, group):
@@ -156,7 +158,12 @@ def main() -> None:
156
158
  cannonical_imports = sorted_imports
157
159
  return cannonical_imports
158
160
 
159
- process_actions(args, options.actions, modify)
161
+ cmdline_exclude = getattr(options, "exclude")
162
+ process_actions(
163
+ args,
164
+ options.actions, modify,
165
+ exclude=default_config.get('tidy-imports', {}).get('exclude', []) + (cmdline_exclude if cmdline_exclude else [])
166
+ )
160
167
 
161
168
 
162
169
  if __name__ == '__main__':
@@ -27,6 +27,10 @@ else:
27
27
  from tomllib import loads
28
28
 
29
29
 
30
+ class ConfigurationError(Exception):
31
+ """Exception class indicating a configuration error."""
32
+
33
+
30
34
  def hfmt(s):
31
35
  return dedent(s).strip()
32
36
 
@@ -232,26 +236,17 @@ def parse_args(addopts=None, import_format_params=False, modify_action_params=Fa
232
236
  only if it is necessary to prevent exceeding the
233
237
  width (by default 79).
234
238
  '''))
235
- def uniform_callback(option, opt_str, value, parser):
236
- parser.values.separate_from_imports = False
237
- parser.values.from_spaces = 3
238
- parser.values.align_imports = '32'
239
- group.add_option('--uniform', '-u', action="callback",
240
- callback=uniform_callback,
239
+ group.add_option('--uniform', '-u', action="store_true",
241
240
  help=hfmt('''
242
241
  (Default) Shortcut for --no-separate-from-imports
243
242
  --from-spaces=3 --align-imports=32.'''))
244
- def unaligned_callback(option, opt_str, value, parser):
245
- parser.values.separate_from_imports = True
246
- parser.values.from_spaces = 1
247
- parser.values.align_imports = '0'
248
- group.add_option('--unaligned', '-n', action="callback",
249
- callback=unaligned_callback,
243
+ group.add_option('--unaligned', '-n', action="store_true",
250
244
  help=hfmt('''
251
245
  Shortcut for --separate-from-imports
252
246
  --from-spaces=1 --align-imports=0.'''))
253
247
 
254
248
  parser.add_option_group(group)
249
+
255
250
  if addopts is not None:
256
251
  addopts(parser)
257
252
  # This is the only way to provide a default value for an option with a
@@ -260,7 +255,22 @@ def parse_args(addopts=None, import_format_params=False, modify_action_params=Fa
260
255
  args = ["--symlinks=error"] + sys.argv[1:]
261
256
  else:
262
257
  args = None
258
+
263
259
  options, args = parser.parse_args(args=args)
260
+
261
+ # Set these manually rather than in a callback option because callback
262
+ # options don't get triggered by OptionParser.set_default (which is
263
+ # used when setting values via pyproject.toml)
264
+ if getattr(options, "unaligned", False):
265
+ parser.values.separate_from_imports = True
266
+ parser.values.from_spaces = 1
267
+ parser.values.align_imports = '0'
268
+
269
+ if getattr(options, "uniform", False):
270
+ parser.values.separate_from_imports = False
271
+ parser.values.from_spaces = 3
272
+ parser.values.align_imports = '32'
273
+
264
274
  if import_format_params:
265
275
  align_imports_args = [int(x.strip())
266
276
  for x in options.align_imports.split(",")]
@@ -380,14 +390,35 @@ class Modifier(object):
380
390
 
381
391
 
382
392
  def process_actions(filenames:List[str], actions, modify_function,
383
- reraise_exceptions=()):
393
+ reraise_exceptions=(), exclude=()):
394
+
395
+ if not isinstance(exclude, (list, tuple)):
396
+ raise ConfigurationError(
397
+ "Exclusions must be a list of filenames/patterns to exclude."
398
+ )
399
+
384
400
  errors = []
385
401
  def on_error_filename_arg(arg):
386
402
  print("%s: bad filename %s" % (sys.argv[0], arg), file=sys.stderr)
387
403
  errors.append("%s: bad filename" % (arg,))
388
- filenames = filename_args(filenames, on_error=on_error_filename_arg)
404
+ filename_objs = filename_args(filenames, on_error=on_error_filename_arg)
389
405
  exit_code = 0
390
- for filename in filenames:
406
+ for filename in filename_objs:
407
+
408
+ # Log any matching exclusion patterns before ignoring, if applicable
409
+ matching_excludes = []
410
+ for pattern in exclude:
411
+ if Path(str(filename)).match(str(pattern)):
412
+ matching_excludes.append(pattern)
413
+ if any(matching_excludes):
414
+ msg = f"{filename} matches exclusion pattern"
415
+ if len(matching_excludes) == 1:
416
+ msg += f": {matching_excludes[0]}"
417
+ else:
418
+ msg += f"s: {matching_excludes}"
419
+ logger.info(msg)
420
+ continue
421
+
391
422
  try:
392
423
  m = Modifier(modify_function, filename)
393
424
  for action in actions:
@@ -533,16 +564,28 @@ symlink_callbacks = {
533
564
  'replace': symlink_replace,
534
565
  }
535
566
 
536
- def _get_pyproj_toml_config():
537
- """
538
- Try to find current project pyproject.toml
567
+ def _get_pyproj_toml_file():
568
+ """Try to find the location of the current project pyproject.toml
539
569
  in cwd or parents directories.
570
+
571
+ If no pyproject.toml can be found, None is returned.
540
572
  """
541
573
  cwd = Path(os.getcwd())
542
574
 
543
575
  for pth in [cwd] + list(cwd.parents):
544
576
  pyproj_toml = pth /'pyproject.toml'
545
577
  if pyproj_toml.exists() and pyproj_toml.is_file():
546
- return loads(pyproj_toml.read_text())
578
+ return pyproj_toml
547
579
 
548
580
  return None
581
+
582
+ def _get_pyproj_toml_config():
583
+ """Return the toml contents of the current pyproject.toml.
584
+
585
+ If no pyproject.toml can be found in cwd or parent directories,
586
+ None is returned.
587
+ """
588
+ pyproject_toml = _get_pyproj_toml_file()
589
+ if pyproject_toml is not None:
590
+ return loads(pyproject_toml.read_text())
591
+ return None
@@ -595,6 +595,12 @@ def _cached_module_finder(
595
595
  Tuples containing (prefix+module name, a bool indicating whether the module is a
596
596
  package or not)
597
597
  """
598
+ if os.environ.get("PYFLYBY_DISABLE_CACHE", "0") == "1":
599
+ modules = _iter_file_finder_modules(importer, SUFFIXES)
600
+ for module, ispkg in modules:
601
+ yield prefix + module, ispkg
602
+ return
603
+
598
604
  cache_dir = pathlib.Path(
599
605
  platformdirs.user_cache_dir(appname='pyflyby', appauthor=False)
600
606
  ) / hashlib.sha256(str(importer.path).encode()).hexdigest()
@@ -610,7 +616,7 @@ def _cached_module_finder(
610
616
  for path in cache_dir.iterdir():
611
617
  _remove_import_cache_dir(path)
612
618
 
613
- if os.environ.get("PYFLYBY_SUPPRESS_CACHE_REBUILD_LOGS", 0) != "1":
619
+ if os.environ.get("PYFLYBY_SUPPRESS_CACHE_REBUILD_LOGS", "1") != "1":
614
620
  logger.info(f"Rebuilding cache for {_format_path(importer.path)}...")
615
621
 
616
622
  modules = _iter_file_finder_modules(importer, SUFFIXES)
@@ -4,4 +4,4 @@
4
4
  # http://creativecommons.org/publicdomain/zero/1.0/
5
5
 
6
6
 
7
- __version__ = "1.10.1"
7
+ __version__ = "1.10.2"
@@ -1,7 +1,7 @@
1
1
  project(
2
2
  'pyflyby',
3
3
  'cpp',
4
- version: '1.10.1',
4
+ version: '1.10.2',
5
5
  default_options: 'cpp_std=c++17'
6
6
  )
7
7
 
@@ -62,24 +62,35 @@ _iter_file_finder_modules(
62
62
  return ret;
63
63
  }
64
64
 
65
- for (auto const &entry : fs::directory_iterator(path)) {
66
- fs::path entry_path = entry.path();
67
- fs::path filename = entry_path.filename();
68
- std::string modname = getmodulename(filename, suffixes);
65
+ // Attempt to iterate the directory. If the directory is unreadable for any reason
66
+ // (e.g., permissions, non-existent, or other system errors), fs::directory_iterator
67
+ // will throw a filesystem_error. We catch this and return an empty list for this path.
68
+ try {
69
+ for (auto const &entry : fs::directory_iterator(path)) {
70
+ fs::path entry_path = entry.path();
71
+ fs::path filename = entry_path.filename();
72
+ std::string modname = getmodulename(filename, suffixes);
69
73
 
70
74
 
71
- if (modname == "" && fs::is_directory(entry_path) &&
72
- filename.string().find(".") == std::string::npos &&
73
- fs::is_regular_file(entry_path / "__init__.py") // Is this a package?
74
- ) {
75
- ret.push_back(std::make_tuple(filename.string(), true));
76
- } else if (modname == "__init__") {
77
- continue;
78
- } else if (modname != "" && modname.find(".") == std::string::npos) {
79
- ret.push_back(std::make_tuple(modname,
80
- false // This is definitely not a package
81
- ));
75
+ if (modname == "" && fs::is_directory(entry_path) &&
76
+ filename.string().find(".") == std::string::npos &&
77
+ fs::is_regular_file(entry_path / "__init__.py") // Is this a package?
78
+ ) {
79
+ ret.push_back(std::make_tuple(filename.string(), true));
80
+ } else if (modname == "__init__") {
81
+ continue;
82
+ } else if (modname != "" && modname.find(".") == std::string::npos) {
83
+ ret.push_back(std::make_tuple(modname,
84
+ false // This is definitely not a package
85
+ ));
86
+ }
82
87
  }
88
+ } catch (const fs::filesystem_error& e) {
89
+ // If an error occurs during directory iteration (e.g., permissions denied,
90
+ // directory removed concurrently), we treat it as unreadable/inaccessible
91
+ // and return the current (potentially empty) list, effectively skipping this path.
92
+ // We could log the error 'e.what()' here if desired for debugging.
93
+ return ret;
83
94
  }
84
95
 
85
96
  return ret;
@@ -934,7 +934,267 @@ def test_load_pyproject_toml(tmp_path, pyproject_text):
934
934
  os.chdir(tmp_path)
935
935
  assert _get_pyproj_toml_config() == loads(pyproject_text)
936
936
 
937
+
937
938
  def test_load_no_pyproject_toml(tmp_path):
938
- """Test that a directory pyproject.toml that has mixed array types can be loaded."""
939
+ """Test that a directory without a pyproject.toml is correctly handled."""
939
940
  os.chdir(tmp_path)
940
941
  assert _get_pyproj_toml_config() is None
942
+
943
+
944
+ def test_pyproject_unaligned(tmp_path):
945
+ """Test that having an unaligned option in pyproject.toml works as intended."""
946
+ with open(tmp_path / 'pyproject.toml', 'w') as f:
947
+ f.write(
948
+ dedent(
949
+ """
950
+ [tool.pyflyby]
951
+ remove_unused = false
952
+ add_mandatory = false
953
+ unaligned = true
954
+ """
955
+ )
956
+ )
957
+
958
+ with open(tmp_path / "foo.py", 'w') as f:
959
+ f.write(
960
+ dedent(
961
+ """
962
+ from math import pi
963
+ import numpy
964
+ from os import open
965
+ import pandas
966
+ from urllib import request
967
+ """
968
+ )
969
+ )
970
+
971
+ child = pexpect.spawn(
972
+ python,
973
+ [BIN_DIR + "/tidy-imports", "./"],
974
+ timeout=5.0,
975
+ cwd=tmp_path,
976
+ logfile=BytesIO(),
977
+ )
978
+ child.expect_exact("foo.py? [y/N]")
979
+ child.send("y\n")
980
+ child.expect(pexpect.EOF)
981
+
982
+ with open(tmp_path / "foo.py") as f:
983
+ assert f.read() == dedent(
984
+ """
985
+ import numpy
986
+ import pandas
987
+ from math import pi
988
+ from os import open
989
+ from urllib import request
990
+ """
991
+ )
992
+
993
+
994
+ def test_no_unaligned(tmp_path):
995
+ """Test that not having an unaligned option in pyproject.toml works as intended."""
996
+ with open(tmp_path / 'pyproject.toml', 'w') as f:
997
+ f.write(
998
+ dedent(
999
+ """
1000
+ [tool.pyflyby]
1001
+ remove_unused = false
1002
+ add_mandatory = false
1003
+ """
1004
+ )
1005
+ )
1006
+
1007
+ with open(tmp_path / "foo.py", 'w') as f:
1008
+ f.write(
1009
+ dedent(
1010
+ """
1011
+ from math import pi
1012
+ import numpy
1013
+ from os import open
1014
+ import pandas
1015
+ from urllib import request
1016
+ """
1017
+ )
1018
+ )
1019
+
1020
+ child = pexpect.spawn(
1021
+ python,
1022
+ [BIN_DIR + "/tidy-imports", "./"],
1023
+ timeout=5.0,
1024
+ cwd=tmp_path,
1025
+ logfile=BytesIO(),
1026
+ )
1027
+ child.expect_exact("foo.py? [y/N]")
1028
+ child.send("y\n")
1029
+ child.expect(pexpect.EOF)
1030
+
1031
+ with open(tmp_path / "foo.py") as f:
1032
+ assert f.read() == dedent(
1033
+ """
1034
+ from math import pi
1035
+ import numpy
1036
+ from os import open
1037
+ import pandas
1038
+ from urllib import request
1039
+ """
1040
+ )
1041
+
1042
+
1043
+ def test_tidy_imports_exclude_pyproject(tmp_path):
1044
+ """Test that a pyproject.toml can be used to exclude files for tidy-imports."""
1045
+ with open(tmp_path / "pyproject.toml", 'w') as f:
1046
+ f.write(
1047
+ dedent(
1048
+ """
1049
+ [tool.pyflyby.tidy-imports]
1050
+ exclude = [
1051
+ 'foo.py',
1052
+ 'bar/*.py',
1053
+ ]
1054
+ """
1055
+ )
1056
+ )
1057
+
1058
+ (tmp_path / "bar").mkdir()
1059
+ (tmp_path / "baz" / "blah").mkdir(parents=True)
1060
+
1061
+ txt = dedent(
1062
+ """
1063
+ # hello
1064
+ def foo():
1065
+ foo() + os + sys
1066
+ """
1067
+ )
1068
+ for path in [
1069
+ tmp_path / "foo.py",
1070
+ tmp_path / "what.py",
1071
+ tmp_path / "bar" / "foo2.py",
1072
+ tmp_path / "baz" / "foo3.py",
1073
+ ]:
1074
+ with open(path, "w") as f:
1075
+ f.write(txt)
1076
+
1077
+ child = pexpect.spawn(
1078
+ python,
1079
+ [BIN_DIR+'/tidy-imports', './'],
1080
+ timeout=5.0,
1081
+ cwd=tmp_path,
1082
+ logfile=BytesIO()
1083
+ )
1084
+ child.expect_exact("baz/foo3.py? [y/N]")
1085
+ child.send("y\n")
1086
+ child.expect_exact("what.py? [y/N]")
1087
+ child.send("y\n")
1088
+ child.expect(pexpect.EOF)
1089
+
1090
+ # Check that the tidy-imports output has log messages about exclusion patterns
1091
+ output = child.logfile.getvalue().decode()
1092
+ assert "bar/foo2.py matches exclusion pattern: bar/*.py" in output
1093
+ assert "foo.py matches exclusion pattern: foo.py" in output
1094
+
1095
+ # Check that the two modified files have imports
1096
+ with open(tmp_path / "baz" / "foo3.py") as f:
1097
+ foo3 = f.read()
1098
+
1099
+ with open(tmp_path / "what.py") as f:
1100
+ what = f.read()
1101
+
1102
+ expected = dedent(
1103
+ """
1104
+ # hello
1105
+ import os
1106
+ import sys
1107
+
1108
+ def foo():
1109
+ foo() + os + sys
1110
+ """
1111
+ )
1112
+ assert foo3 == expected
1113
+ assert what == expected
1114
+
1115
+ # Check that the two unmodified files don't have imports
1116
+ with open(tmp_path / "foo.py") as f:
1117
+ foo = f.read()
1118
+
1119
+ with open(tmp_path / "bar" / "foo2.py") as f:
1120
+ foo2 = f.read()
1121
+
1122
+ assert foo == txt
1123
+ assert foo2 == txt
1124
+
1125
+
1126
+ def test_tidy_imports_exclude_arg(tmp_path):
1127
+ """Test that a command line arg can be used to exclude files for tidy-imports."""
1128
+ (tmp_path / "bar").mkdir()
1129
+ (tmp_path / "baz" / "blah").mkdir(parents=True)
1130
+
1131
+ txt = dedent(
1132
+ """
1133
+ # hello
1134
+ def foo():
1135
+ foo() + os + sys
1136
+ """
1137
+ )
1138
+ for path in [
1139
+ tmp_path / "foo.py",
1140
+ tmp_path / "what.py",
1141
+ tmp_path / "bar" / "foo2.py",
1142
+ tmp_path / "baz" / "foo3.py",
1143
+ ]:
1144
+ with open(path, "w") as f:
1145
+ f.write(txt)
1146
+
1147
+ child = pexpect.spawn(
1148
+ python,
1149
+ [
1150
+ BIN_DIR + "/tidy-imports",
1151
+ "./",
1152
+ "--exclude",
1153
+ "foo.py",
1154
+ "--exclude",
1155
+ "bar/*.py",
1156
+ ],
1157
+ timeout=5.0,
1158
+ cwd=tmp_path,
1159
+ logfile=BytesIO(),
1160
+ )
1161
+ child.expect_exact("baz/foo3.py? [y/N]")
1162
+ child.send("y\n")
1163
+ child.expect_exact("what.py? [y/N]")
1164
+ child.send("y\n")
1165
+ child.expect(pexpect.EOF)
1166
+
1167
+ # Check that the tidy-imports output has log messages about exclusion patterns
1168
+ output = child.logfile.getvalue().decode()
1169
+ assert "bar/foo2.py matches exclusion pattern: bar/*.py" in output
1170
+ assert "foo.py matches exclusion pattern: foo.py" in output
1171
+
1172
+ # Check that the two modified files have imports
1173
+ with open(tmp_path / "baz" / "foo3.py") as f:
1174
+ foo3 = f.read()
1175
+
1176
+ with open(tmp_path / "what.py") as f:
1177
+ what = f.read()
1178
+
1179
+ expected = dedent(
1180
+ """
1181
+ # hello
1182
+ import os
1183
+ import sys
1184
+
1185
+ def foo():
1186
+ foo() + os + sys
1187
+ """
1188
+ )
1189
+ assert foo3 == expected
1190
+ assert what == expected
1191
+
1192
+ # Check that the two unmodified files don't have imports
1193
+ with open(tmp_path / "foo.py") as f:
1194
+ foo = f.read()
1195
+
1196
+ with open(tmp_path / "bar" / "foo2.py") as f:
1197
+ foo2 = f.read()
1198
+
1199
+ assert foo == txt
1200
+ assert foo2 == txt
@@ -13,9 +13,11 @@ from pyflyby._log import logger
13
13
  from pyflyby._modules import (ModuleHandle, _fast_iter_modules,
14
14
  _iter_file_finder_modules)
15
15
  import re
16
+ import os
16
17
  import subprocess
17
18
  import sys
18
19
  from textwrap import dedent
20
+ from tempfile import TemporaryDirectory
19
21
  from unittest import mock
20
22
 
21
23
  import pytest
@@ -122,7 +124,7 @@ def test_fast_iter_modules():
122
124
 
123
125
  assert fast == slow
124
126
 
125
-
127
+ @mock.patch.dict(os.environ, {"PYFLYBY_SUPPRESS_CACHE_REBUILD_LOGS": "0"})
126
128
  @mock.patch("platformdirs.user_cache_dir")
127
129
  def test_import_cache(mock_user_cache_dir, tmp_path):
128
130
  """Test that the import cache is built when iterating modules.
@@ -195,3 +197,20 @@ def test_import_cache(mock_user_cache_dir, tmp_path):
195
197
  assert len(mock_logger.info.call_args_list) == 1
196
198
  assert len(list(tmp_path.iterdir())) == n_cached_paths
197
199
  mock_iffm.assert_called_once()
200
+
201
+ @mock.patch.dict(os.environ, {"PYFLYBY_DISABLE_CACHE": "1"})
202
+ @mock.patch("platformdirs.user_cache_dir")
203
+ def test_import_perms(mock_user_cache_dir, tmp_path):
204
+ """Test that the import cache does not fail on unreadable paths."""
205
+
206
+ mock_user_cache_dir.return_value = tmp_path
207
+
208
+ with TemporaryDirectory(suffix="_pyflyby_restricted") as restricted:
209
+ try:
210
+ os.chmod(restricted, 0o000)
211
+
212
+ sys.path.append(restricted)
213
+
214
+ list(_fast_iter_modules())
215
+ finally:
216
+ sys.path.remove(restricted)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes