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