pyflyby 1.10.1__cp311-cp311-manylinux_2_24_x86_64.whl

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 (55) hide show
  1. pyflyby/__init__.py +61 -0
  2. pyflyby/__main__.py +9 -0
  3. pyflyby/_autoimp.py +2229 -0
  4. pyflyby/_cmdline.py +548 -0
  5. pyflyby/_comms.py +221 -0
  6. pyflyby/_dbg.py +1367 -0
  7. pyflyby/_docxref.py +379 -0
  8. pyflyby/_dynimp.py +154 -0
  9. pyflyby/_fast_iter_modules.cpython-311-x86_64-linux-gnu.so +0 -0
  10. pyflyby/_file.py +771 -0
  11. pyflyby/_flags.py +230 -0
  12. pyflyby/_format.py +186 -0
  13. pyflyby/_idents.py +227 -0
  14. pyflyby/_import_sorting.py +165 -0
  15. pyflyby/_importclns.py +658 -0
  16. pyflyby/_importdb.py +680 -0
  17. pyflyby/_imports2s.py +643 -0
  18. pyflyby/_importstmt.py +723 -0
  19. pyflyby/_interactive.py +2113 -0
  20. pyflyby/_livepatch.py +793 -0
  21. pyflyby/_log.py +104 -0
  22. pyflyby/_modules.py +641 -0
  23. pyflyby/_parse.py +1381 -0
  24. pyflyby/_py.py +2166 -0
  25. pyflyby/_saveframe.py +1145 -0
  26. pyflyby/_saveframe_reader.py +471 -0
  27. pyflyby/_util.py +458 -0
  28. pyflyby/_version.py +7 -0
  29. pyflyby/autoimport.py +20 -0
  30. pyflyby/etc/pyflyby/canonical.py +10 -0
  31. pyflyby/etc/pyflyby/common.py +27 -0
  32. pyflyby/etc/pyflyby/forget.py +10 -0
  33. pyflyby/etc/pyflyby/mandatory.py +10 -0
  34. pyflyby/etc/pyflyby/numpy.py +156 -0
  35. pyflyby/etc/pyflyby/std.py +335 -0
  36. pyflyby/importdb.py +19 -0
  37. pyflyby/libexec/pyflyby/colordiff +34 -0
  38. pyflyby/libexec/pyflyby/diff-colorize +148 -0
  39. pyflyby/share/emacs/site-lisp/pyflyby.el +108 -0
  40. pyflyby-1.10.1.data/scripts/collect-exports +76 -0
  41. pyflyby-1.10.1.data/scripts/collect-imports +58 -0
  42. pyflyby-1.10.1.data/scripts/find-import +38 -0
  43. pyflyby-1.10.1.data/scripts/list-bad-xrefs +34 -0
  44. pyflyby-1.10.1.data/scripts/prune-broken-imports +34 -0
  45. pyflyby-1.10.1.data/scripts/pyflyby-diff +34 -0
  46. pyflyby-1.10.1.data/scripts/reformat-imports +27 -0
  47. pyflyby-1.10.1.data/scripts/replace-star-imports +37 -0
  48. pyflyby-1.10.1.data/scripts/saveframe +299 -0
  49. pyflyby-1.10.1.data/scripts/tidy-imports +163 -0
  50. pyflyby-1.10.1.data/scripts/transform-imports +47 -0
  51. pyflyby-1.10.1.dist-info/METADATA +591 -0
  52. pyflyby-1.10.1.dist-info/RECORD +55 -0
  53. pyflyby-1.10.1.dist-info/WHEEL +5 -0
  54. pyflyby-1.10.1.dist-info/entry_points.txt +4 -0
  55. pyflyby-1.10.1.dist-info/licenses/LICENSE.txt +23 -0
pyflyby/_cmdline.py ADDED
@@ -0,0 +1,548 @@
1
+ # pyflyby/_cmdline.py.
2
+ # Copyright (C) 2011, 2012, 2013, 2014, 2015, 2018 Karl Chen.
3
+ # License: MIT http://opensource.org/licenses/MIT
4
+
5
+
6
+
7
+ from builtins import input
8
+ import optparse
9
+ import os
10
+ from pathlib import Path
11
+ import signal
12
+ import sys
13
+ from textwrap import dedent
14
+ import traceback
15
+ from typing import List
16
+
17
+
18
+ from pyflyby._file import (FileText, Filename, atomic_write_file,
19
+ expand_py_files_from_args, read_file)
20
+ from pyflyby._importstmt import ImportFormatParams
21
+ from pyflyby._log import logger
22
+ from pyflyby._util import cached_attribute, indent
23
+
24
+ if sys.version_info < (3, 11):
25
+ from tomli import loads
26
+ else:
27
+ from tomllib import loads
28
+
29
+
30
+ def hfmt(s):
31
+ return dedent(s).strip()
32
+
33
+ def maindoc():
34
+ import __main__
35
+ return (__main__.__doc__ or '').strip()
36
+
37
+
38
+ def _sigpipe_handler(*args):
39
+ # The parent process piped our stdout and closed the pipe before we
40
+ # finished writing, e.g. "tidy-imports ... | head" or "tidy-imports ... |
41
+ # less". Exit quietly - squelch the "close failed in file object
42
+ # destructor" message would otherwise be raised.
43
+ raise SystemExit(1)
44
+
45
+
46
+ def parse_args(addopts=None, import_format_params=False, modify_action_params=False):
47
+ """
48
+ Do setup for a top-level script and parse arguments.
49
+ """
50
+ ### Setup.
51
+ # Register a SIGPIPE handler.
52
+ signal.signal(signal.SIGPIPE, _sigpipe_handler)
53
+ ### Parse args.
54
+ parser = optparse.OptionParser(usage='\n'+maindoc())
55
+
56
+ def log_level_callbacker(level):
57
+ def callback(option, opt_str, value, parser):
58
+ logger.set_level(level)
59
+ return callback
60
+
61
+ def debug_callback(option, opt_str, value, parser):
62
+ logger.set_level("DEBUG")
63
+
64
+ parser.add_option("--debug", action="callback",
65
+ callback=debug_callback,
66
+ help="Debug mode (noisy and fail fast).")
67
+
68
+ parser.add_option("--verbose", action="callback",
69
+ callback=log_level_callbacker("DEBUG"),
70
+ help="Be noisy.")
71
+
72
+ parser.add_option("--quiet", action="callback",
73
+ callback=log_level_callbacker("ERROR"),
74
+ help="Be quiet.")
75
+
76
+ parser.add_option("--version", action="callback",
77
+ callback=lambda *args: print_version_and_exit(),
78
+ help="Print pyflyby version and exit.")
79
+
80
+ if modify_action_params:
81
+ group = optparse.OptionGroup(parser, "Action options")
82
+ action_diff = action_external_command('pyflyby-diff')
83
+ def parse_action(v):
84
+ V = v.strip().upper()
85
+ if V == 'PRINT':
86
+ return action_print
87
+ elif V == 'REPLACE':
88
+ return action_replace
89
+ elif V == 'QUERY':
90
+ return action_query()
91
+ elif V == "DIFF":
92
+ return action_diff
93
+ elif V.startswith("QUERY:"):
94
+ return action_query(v[6:])
95
+ elif V.startswith("EXECUTE:"):
96
+ return action_external_command(v[8:])
97
+ elif V == "IFCHANGED":
98
+ return action_ifchanged
99
+ elif V == "EXIT1":
100
+ return action_exit1
101
+ else:
102
+ raise Exception(
103
+ "Bad argument %r to --action; "
104
+ "expected PRINT or REPLACE or QUERY or IFCHANGED or EXIT1 "
105
+ "or EXECUTE:..." % (v,))
106
+
107
+ def set_actions(actions):
108
+ actions = tuple(actions)
109
+ parser.values.actions = actions
110
+
111
+ def action_callback(option, opt_str, value, parser):
112
+ action_args = value.split(',')
113
+ set_actions([parse_action(v) for v in action_args])
114
+
115
+ def action_callbacker(actions):
116
+ def callback(option, opt_str, value, parser):
117
+ set_actions(actions)
118
+ return callback
119
+
120
+ group.add_option(
121
+ "--actions", type='string', action='callback',
122
+ callback=action_callback,
123
+ metavar='PRINT|REPLACE|IFCHANGED|QUERY|DIFF|EXIT1:EXECUTE:mycommand',
124
+ help=hfmt('''
125
+ Comma-separated list of action(s) to take. If PRINT, print
126
+ the changed file to stdout. If REPLACE, then modify the
127
+ file in-place. If EXECUTE:mycommand, then execute
128
+ 'mycommand oldfile tmpfile'. If DIFF, then execute
129
+ 'pyflyby-diff'. If QUERY, then query user to continue.
130
+ If IFCHANGED, then continue actions only if file was
131
+ changed. If EXIT1, then exit with exit code 1 after all
132
+ files/actions are processed.'''))
133
+ group.add_option(
134
+ "--print", "-p", action='callback',
135
+ callback=action_callbacker([action_print]),
136
+ help=hfmt('''
137
+ Equivalent to --action=PRINT (default when stdin or stdout is
138
+ not a tty) '''))
139
+ group.add_option(
140
+ "--diff", "-d", action='callback',
141
+ callback=action_callbacker([action_diff]),
142
+ help=hfmt('''Equivalent to --action=DIFF'''))
143
+ group.add_option(
144
+ "--replace", "-r", action='callback',
145
+ callback=action_callbacker([action_ifchanged, action_replace]),
146
+ help=hfmt('''Equivalent to --action=IFCHANGED,REPLACE'''))
147
+ group.add_option(
148
+ "--diff-replace", "-R", action='callback',
149
+ callback=action_callbacker([action_ifchanged, action_diff, action_replace]),
150
+ help=hfmt('''Equivalent to --action=IFCHANGED,DIFF,REPLACE'''))
151
+ actions_interactive = [
152
+ action_ifchanged, action_diff,
153
+ action_query("Replace {filename}?"), action_replace]
154
+ group.add_option(
155
+ "--interactive", "-i", action='callback',
156
+ callback=action_callbacker(actions_interactive),
157
+ help=hfmt('''
158
+ Equivalent to --action=IFCHANGED,DIFF,QUERY,REPLACE (default
159
+ when stdin & stdout are ttys) '''))
160
+ if os.isatty(0) and os.isatty(1):
161
+ default_actions = actions_interactive
162
+ else:
163
+ default_actions = [action_print]
164
+ parser.set_default('actions', tuple(default_actions))
165
+ parser.add_option_group(group)
166
+
167
+ parser.add_option(
168
+ '--symlinks', action='callback', nargs=1, type=str,
169
+ dest='symlinks', callback=symlink_callback, help="--symlinks should be one of: " + symlinks_help,
170
+ )
171
+ parser.set_defaults(symlinks='error')
172
+
173
+ if import_format_params:
174
+ group = optparse.OptionGroup(parser, "Pretty-printing options")
175
+ group.add_option('--align-imports', '--align', type='str', default="32",
176
+ metavar='N',
177
+ help=hfmt('''
178
+ Whether and how to align the 'import' keyword in
179
+ 'from modulename import aliases...'. If 0, then
180
+ don't align. If 1, then align within each block
181
+ of imports. If an integer > 1, then align at
182
+ that column, wrapping with a backslash if
183
+ necessary. If a comma-separated list of integers
184
+ (tab stops), then pick the column that results in
185
+ the fewest number of lines total per block.'''))
186
+ group.add_option('--from-spaces', type='int', default=3, metavar='N',
187
+ help=hfmt('''
188
+ The number of spaces after the 'from' keyword.
189
+ (Must be at least 1; default is 3.)'''))
190
+ group.add_option('--separate-from-imports', action='store_true',
191
+ default=False,
192
+ help=hfmt('''
193
+ Separate 'from ... import ...'
194
+ statements from 'import ...' statements.'''))
195
+ group.add_option('--no-separate-from-imports', action='store_false',
196
+ dest='separate_from_imports',
197
+ help=hfmt('''
198
+ (Default) Don't separate 'from ... import ...'
199
+ statements from 'import ...' statements.'''))
200
+ group.add_option('--align-future', action='store_true',
201
+ default=False,
202
+ help=hfmt('''
203
+ Align the 'from __future__ import ...' statement
204
+ like others.'''))
205
+ group.add_option('--no-align-future', action='store_false',
206
+ dest='align_future',
207
+ help=hfmt('''
208
+ (Default) Don't align the 'from __future__ import
209
+ ...' statement.'''))
210
+ group.add_option('--width', type='int', default=None, metavar='N',
211
+ help=hfmt('''
212
+ Maximum line length (default: 79).'''))
213
+ group.add_option('--black', action='store_true', default=False,
214
+ help=hfmt('''
215
+ Use black to format imports. If this option is
216
+ used, all other formatting options are ignored,
217
+ except width'''))
218
+ group.add_option('--hanging-indent', type='choice', default='never',
219
+ choices=['never','auto','always'],
220
+ metavar='never|auto|always',
221
+ dest='hanging_indent',
222
+ help=hfmt('''
223
+ How to wrap import statements that don't fit on
224
+ one line.
225
+ If --hanging-indent=always, then always indent
226
+ imported tokens at column 4 on the next line.
227
+ If --hanging-indent=never (default), then align
228
+ import tokens after "import (" (by default column
229
+ 40); do so even if some symbols are so long that
230
+ this would exceed the width (by default 79)).
231
+ If --hanging-indent=auto, then use hanging indent
232
+ only if it is necessary to prevent exceeding the
233
+ width (by default 79).
234
+ '''))
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,
241
+ help=hfmt('''
242
+ (Default) Shortcut for --no-separate-from-imports
243
+ --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,
250
+ help=hfmt('''
251
+ Shortcut for --separate-from-imports
252
+ --from-spaces=1 --align-imports=0.'''))
253
+
254
+ parser.add_option_group(group)
255
+ if addopts is not None:
256
+ addopts(parser)
257
+ # This is the only way to provide a default value for an option with a
258
+ # callback.
259
+ if modify_action_params:
260
+ args = ["--symlinks=error"] + sys.argv[1:]
261
+ else:
262
+ args = None
263
+ options, args = parser.parse_args(args=args)
264
+ if import_format_params:
265
+ align_imports_args = [int(x.strip())
266
+ for x in options.align_imports.split(",")]
267
+ if len(align_imports_args) == 1 and align_imports_args[0] == 1:
268
+ align_imports = True
269
+ elif len(align_imports_args) == 1 and align_imports_args[0] == 0:
270
+ align_imports = False
271
+ else:
272
+ align_imports = tuple(sorted(set(align_imports_args)))
273
+ options.params = ImportFormatParams(
274
+ align_imports =align_imports,
275
+ from_spaces =options.from_spaces,
276
+ separate_from_imports =options.separate_from_imports,
277
+ max_line_length =options.width,
278
+ use_black =options.black,
279
+ align_future =options.align_future,
280
+ hanging_indent =options.hanging_indent,
281
+ )
282
+ return options, args
283
+
284
+
285
+ def _default_on_error(filename):
286
+ raise SystemExit("bad filename %s" % (filename,))
287
+
288
+
289
+ def filename_args(args: List[str], on_error=_default_on_error):
290
+ """
291
+ Return list of filenames given command-line arguments.
292
+
293
+ :rtype:
294
+ ``list`` of `Filename`
295
+ """
296
+ if args:
297
+ for a in args:
298
+ assert isinstance(a, str)
299
+ return expand_py_files_from_args([Filename(f) for f in args], on_error)
300
+ elif not os.isatty(0):
301
+ return [Filename.STDIN]
302
+ else:
303
+ syntax()
304
+
305
+
306
+ def print_version_and_exit(extra=None):
307
+ from pyflyby._version import __version__
308
+ msg = "pyflyby %s" % (__version__,)
309
+ progname = os.path.realpath(sys.argv[0])
310
+ if os.path.exists(progname):
311
+ msg += " (%s)" % (os.path.basename(progname),)
312
+ print(msg)
313
+ if extra:
314
+ print(extra)
315
+ raise SystemExit(0)
316
+
317
+
318
+ def syntax(message=None, usage=None):
319
+ if message:
320
+ logger.error(message)
321
+ outmsg = ((usage or maindoc()) +
322
+ '\n\nFor usage, see: %s --help' % (sys.argv[0],))
323
+ print(outmsg, file=sys.stderr)
324
+ raise SystemExit(1)
325
+
326
+
327
+ class AbortActions(Exception):
328
+ pass
329
+
330
+
331
+ class Exit1(Exception):
332
+ pass
333
+
334
+
335
+ class Modifier(object):
336
+ def __init__(self, modifier, filename):
337
+ self.modifier = modifier
338
+ self.filename = filename
339
+ self._tmpfiles = []
340
+
341
+ @cached_attribute
342
+ def input_content(self):
343
+ return read_file(self.filename)
344
+
345
+ # TODO: refactor to avoid having these heavy-weight things inside a
346
+ # cached_attribute, which causes annoyance while debugging.
347
+ @cached_attribute
348
+ def output_content(self):
349
+ return FileText(self.modifier(self.input_content), filename=self.filename)
350
+
351
+ def _tempfile(self):
352
+ from tempfile import NamedTemporaryFile
353
+ f = NamedTemporaryFile()
354
+ self._tmpfiles.append(f)
355
+ return f, Filename(f.name)
356
+
357
+
358
+ @cached_attribute
359
+ def output_content_filename(self):
360
+ f, fname = self._tempfile()
361
+ f.write(bytes(self.output_content.joined, "utf-8"))
362
+ f.flush()
363
+ return fname
364
+
365
+ @cached_attribute
366
+ def input_content_filename(self):
367
+ if isinstance(self.filename, Filename):
368
+ return self.filename
369
+ # If the input was stdin, and the user wants a diff, then we need to
370
+ # write it to a temp file.
371
+ f, fname = self._tempfile()
372
+ f.write(bytes(self.input_content, "utf-8"))
373
+ f.flush()
374
+ return fname
375
+
376
+
377
+ def __del__(self):
378
+ for f in self._tmpfiles:
379
+ f.close()
380
+
381
+
382
+ def process_actions(filenames:List[str], actions, modify_function,
383
+ reraise_exceptions=()):
384
+ errors = []
385
+ def on_error_filename_arg(arg):
386
+ print("%s: bad filename %s" % (sys.argv[0], arg), file=sys.stderr)
387
+ errors.append("%s: bad filename" % (arg,))
388
+ filenames = filename_args(filenames, on_error=on_error_filename_arg)
389
+ exit_code = 0
390
+ for filename in filenames:
391
+ try:
392
+ m = Modifier(modify_function, filename)
393
+ for action in actions:
394
+ action(m)
395
+ except AbortActions:
396
+ continue
397
+ except reraise_exceptions:
398
+ raise
399
+ except Exit1:
400
+ exit_code = 1
401
+ except Exception as e:
402
+ errors.append("%s: %s: %s" % (filename, type(e).__name__, e))
403
+ type_e = type(e)
404
+ try:
405
+ tb = sys.exc_info()[2]
406
+ if str(filename) not in str(e):
407
+ try:
408
+ e = type_e("While processing %s: %s" % (filename, e))
409
+ pass
410
+ except TypeError:
411
+ # Exception takes more than one argument
412
+ pass
413
+ if logger.debug_enabled:
414
+ raise
415
+ traceback.print_exception(type(e), e, tb)
416
+ finally:
417
+ tb = None # avoid refcycles involving tb
418
+ continue
419
+ if errors:
420
+ msg = "\n%s: encountered the following problems:\n" % (sys.argv[0],)
421
+ for er in errors:
422
+ lines = er.splitlines()
423
+ msg += " " + lines[0] + '\n'.join(
424
+ (" %s"%line for line in lines[1:]))
425
+ raise SystemExit(msg)
426
+ else:
427
+ raise SystemExit(exit_code)
428
+
429
+
430
+ def action_print(m):
431
+ output_content = m.output_content
432
+ sys.stdout.write(output_content.joined)
433
+
434
+
435
+ def action_ifchanged(m):
436
+ if m.output_content.joined == m.input_content.joined:
437
+ logger.debug("unmodified: %s", m.filename)
438
+ raise AbortActions
439
+
440
+
441
+ def action_replace(m):
442
+ if m.filename == Filename.STDIN:
443
+ raise Exception("Can't replace stdio in-place")
444
+ logger.info("%s: *** modified ***", m.filename)
445
+ atomic_write_file(m.filename, m.output_content)
446
+
447
+
448
+ def action_exit1(m):
449
+ logger.debug("action_exit1")
450
+ raise Exit1
451
+
452
+
453
+ def action_external_command(command):
454
+ import subprocess
455
+ def action(m):
456
+ bindir = os.path.dirname(os.path.realpath(sys.argv[0]))
457
+ env = os.environ
458
+ env['PATH'] = env['PATH'] + ":" + bindir
459
+ fullcmd = "%s %s %s" % (
460
+ command, m.input_content_filename, m.output_content_filename)
461
+ logger.debug("Executing external command: %s", fullcmd)
462
+ ret = subprocess.call(fullcmd, shell=True, env=env)
463
+ logger.debug("External command returned %d", ret)
464
+ return action
465
+
466
+
467
+ def action_query(prompt="Proceed?"):
468
+ def action(m):
469
+ p = prompt.format(filename=m.filename)
470
+ print()
471
+ print("%s [y/N] " % (p), end="")
472
+ try:
473
+ if input().strip().lower().startswith('y'):
474
+ return True
475
+ except KeyboardInterrupt:
476
+ print("KeyboardInterrupt", file=sys.stderr)
477
+ raise SystemExit(1)
478
+ print("Aborted")
479
+ raise AbortActions
480
+ return action
481
+
482
+ def symlink_callback(option, opt_str, value, parser):
483
+ parser.values.actions = tuple(i for i in parser.values.actions if i not in
484
+ symlink_callbacks.values())
485
+ if value in symlink_callbacks:
486
+ parser.values.actions = (symlink_callbacks[value],) + parser.values.actions
487
+ else:
488
+ raise optparse.OptionValueError("--symlinks must be one of 'error', 'follow', 'skip', or 'replace'. Got %r" % value)
489
+
490
+ symlinks_help = """\
491
+ --symlinks=error (default; gives an error on symlinks),
492
+ --symlinks=follow (follows symlinks),
493
+ --symlinks=skip (skips symlinks),
494
+ --symlinks=replace (replaces symlinks with the target file\
495
+ """
496
+
497
+ # Warning, the symlink actions will only work if they are run first.
498
+ # Otherwise, output_content may already be cached
499
+ def symlink_error(m):
500
+ if m.filename == Filename.STDIN:
501
+ return symlink_follow(m)
502
+ if m.filename.islink:
503
+ raise SystemExit("""\
504
+ Error: %s appears to be a symlink. Use one of the following options to allow symlinks:
505
+ %s
506
+ """ % (m.filename, indent(symlinks_help, ' ')))
507
+
508
+ def symlink_follow(m):
509
+ if m.filename == Filename.STDIN:
510
+ return
511
+ if m.filename.islink:
512
+ logger.info("Following symlink %s" % m.filename)
513
+ m.filename = m.filename.realpath
514
+
515
+ def symlink_skip(m):
516
+ if m.filename == Filename.STDIN:
517
+ return symlink_follow(m)
518
+ if m.filename.islink:
519
+ logger.info("Skipping symlink %s" % m.filename)
520
+ raise AbortActions
521
+
522
+ def symlink_replace(m):
523
+ if m.filename == Filename.STDIN:
524
+ return symlink_follow(m)
525
+ if m.filename.islink:
526
+ logger.info("Replacing symlink %s" % m.filename)
527
+ # The current behavior automatically replaces symlinks, so do nothing
528
+
529
+ symlink_callbacks = {
530
+ 'error': symlink_error,
531
+ 'follow': symlink_follow,
532
+ 'skip': symlink_skip,
533
+ 'replace': symlink_replace,
534
+ }
535
+
536
+ def _get_pyproj_toml_config():
537
+ """
538
+ Try to find current project pyproject.toml
539
+ in cwd or parents directories.
540
+ """
541
+ cwd = Path(os.getcwd())
542
+
543
+ for pth in [cwd] + list(cwd.parents):
544
+ pyproj_toml = pth /'pyproject.toml'
545
+ if pyproj_toml.exists() and pyproj_toml.is_file():
546
+ return loads(pyproj_toml.read_text())
547
+
548
+ return None