squared 0.4.13 → 0.4.15

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.
@@ -8,7 +8,7 @@ module Squared
8
8
  DIR_PYTHON = (DEP_PYTHON + %w[README.rst]).freeze
9
9
  OPT_PYTHON = {
10
10
  common: %w[b B d E h i I O OO P q s S u v x c=q m=b W=b X=q check-hash-based-pycs=b].freeze,
11
- build: %w[n|no-isolation s|sdist v|verbose w|wheel x|skip-dependency-check C|config-setting=q installer=b
11
+ build: %w[n|no-isolation s|sdist x|skip-dependency-check v|verbose w|wheel C|config-setting=q installer=b
12
12
  o|outdir=p].freeze,
13
13
  venv: %w[clear copies symlinks system-site-packages upgrade upgrade-deps without-scm-ignore-files without-pip
14
14
  prompt=q].freeze
@@ -28,11 +28,18 @@ module Squared
28
28
  freeze: %w[all exclude-editable l|local user exclude=b path=p r|requirement=p].freeze
29
29
  }.freeze
30
30
  OPT_POETRY = {
31
- common: %w[ansi no-ansi no-cache n|no-interaction no-plugins P|project=p q|quiet v|verbose].freeze,
31
+ common: %w[ansi no-ansi no-cache n|no-interaction no-plugins q|quiet v|verbose P|project=p].freeze,
32
32
  build: %w[clean config-settings=qq f|format=b o|output=p].freeze,
33
- publish: %w[build dry-run client-cert=p cert=p dist-dir=p p|password=b r|repository=b skip-existing
33
+ publish: %w[build dry-run skip-existing cert=p client-cert=p dist-dir=p p|password=b r|repository=b
34
34
  u|username=b].freeze
35
35
  }.freeze
36
+ OPT_PDM = {
37
+ common: %w[I|ignore-python no-cache n|non-interactive].freeze,
38
+ build: %w[C=bm no-clean no-isolation no-sdist no-wheel quiet verbose config-setting=q d|dest=p p|project=p
39
+ k|skip=b].freeze,
40
+ publish: %w[no-build no-very-ssl quiet S|sign skip-existing verbose ca-certs=p c|comment=q d|dest=p identity=b
41
+ p|password=q p|project=p r|repository=q k|skip=b u|username=b].freeze
42
+ }.freeze
36
43
  OPT_HATCH = {
37
44
  common: %w[color interactive no-color no-interactive cache-dir=p config=p data-dir=p e|env=b p|project=b
38
45
  q|quiet v|verbose].freeze,
@@ -41,11 +48,11 @@ module Squared
41
48
  p|publisher=b r|repo=b u|user=q].freeze
42
49
  }.freeze
43
50
  OPT_TWINE = {
44
- publish: %w[attestations disable-progress-bar non-interactive skip-existing verbose s|sign c|comment=q
45
- config-file=p cert=p client-cert=p i|identity=b p|password=q r|repository=b repository-url=q
51
+ publish: %w[attestations disable-progress-bar non-interactive s|sign skip-existing verbose cert=p
52
+ client-cert=p c|comment=q config-file=p i|identity=b p|password=q r|repository=b repository-url=q
46
53
  sign-with=b u|username=q].freeze
47
54
  }.freeze
48
- private_constant :DEP_PYTHON, :DIR_PYTHON, :OPT_PYTHON, :OPT_PIP, :OPT_POETRY, :OPT_HATCH, :OPT_TWINE
55
+ private_constant :DEP_PYTHON, :DIR_PYTHON, :OPT_PYTHON, :OPT_PIP, :OPT_POETRY, :OPT_PDM, :OPT_HATCH, :OPT_TWINE
49
56
 
50
57
  class << self
51
58
  def populate(*); end
@@ -71,7 +78,7 @@ module Squared
71
78
 
72
79
  attr_reader :venv, :editable
73
80
 
74
- def initialize(*, venv: nil, editable: '.', verbose: nil, **kwargs)
81
+ def initialize(*, editable: '.', verbose: nil, **kwargs)
75
82
  super
76
83
  if @pass.include?(Python.ref)
77
84
  initialize_ref Python.ref
@@ -80,18 +87,20 @@ module Squared
80
87
  initialize_build(Python.ref, **kwargs)
81
88
  initialize_env(**kwargs)
82
89
  end
83
- dependfile_set DEP_PYTHON
84
90
  @verbose = verbose.size if verbose.is_a?(String) && verbose.match?(/\Av+\z/)
91
+ dependfile_set DEP_PYTHON
85
92
  editable_set editable
86
- venv_set venv if venv
93
+ venv_set kwargs[:venv]
87
94
  end
88
95
 
89
96
  subtasks({
90
97
  'venv' => %i[exec create remove show].freeze,
91
98
  'pip' => %i[uninstall freeze].freeze,
92
99
  'install' => %i[user force upgrade target editable].freeze,
93
- 'build' => %i[python poetry hatch].freeze,
94
- 'publish' => %i[poetry twine hatch].freeze,
100
+ 'outdated' => %i[major minor patch].freeze,
101
+ 'build' => %i[python poetry pdm hatch].freeze,
102
+ 'publish' => %i[poetry pdm hatch twine].freeze,
103
+ 'run' => nil,
95
104
  'exec' => nil
96
105
  })
97
106
 
@@ -109,6 +118,62 @@ module Squared
109
118
 
110
119
  if flags.nil?
111
120
  case action
121
+ when 'run'
122
+ next unless pyprojectfile
123
+
124
+ format_desc action, nil, "script+|#{indexchar}index+|#,pattern*"
125
+ task action, [:command] do |_, args|
126
+ found = 0
127
+ ['tool.poetry.scripts', 'tool.pdm.scripts', 'project.scripts'].each_with_index do |table, index|
128
+ next if (list = read_pyproject(table)).empty?
129
+
130
+ if args.command == '#'
131
+ format_list(list, "run[#{indexchar}N]", 'scripts', grep: args.extras, from: pyprojectfile)
132
+ found |= 1
133
+ else
134
+ args.to_a.each do |val|
135
+ if (n, = indexitem(val))
136
+ if (script, = list[n - 1])
137
+ case index
138
+ when 0
139
+ script = session_output 'poetry', 'run', script
140
+ when 1
141
+ script = pdm_session 'run', script
142
+ else
143
+ venv_init
144
+ end
145
+ found |= 1
146
+ run(script, from: :run)
147
+ elsif exception
148
+ indexerror n, list
149
+ else
150
+ found |= 2
151
+ log.warn "run script #{n} of #{list.size} (out of range)"
152
+ end
153
+ else
154
+ case index
155
+ when 0
156
+ found |= 1
157
+ run(session_output('poetry', 'run', val), from: :run)
158
+ when 1
159
+ found |= 1
160
+ run(pdm_session('run', val), from: :run)
161
+ else
162
+ raise_error("script: #{val}", hint: 'unknown') if exception
163
+ found |= 2
164
+ log.warn "run script \"#{val}\" (not indexed)"
165
+ end
166
+ end
167
+ end
168
+ end
169
+ break
170
+ end
171
+ unless found.anybits?(1)
172
+ puts log_message(found == 0 ? Logger::INFO : Logger.WARN,
173
+ "no scripts #{found == 0 ? 'found' : 'executed'}",
174
+ subject: name, hint: pyprojectfile)
175
+ end
176
+ end
112
177
  when 'exec'
113
178
  format_desc action, nil, 'command|:,args*'
114
179
  task action do |_, args|
@@ -123,7 +188,7 @@ module Squared
123
188
  end
124
189
  args.join(' ')
125
190
  end
126
- Kernel.exec(cmd, chdir: path)
191
+ shell(cmd, name: :exec, chdir: path)
127
192
  end
128
193
  end
129
194
  else
@@ -214,14 +279,30 @@ module Squared
214
279
  depend flag, args.to_a
215
280
  end
216
281
  end
282
+ when 'outdated'
283
+ format_desc action, flag, 'eager?,user?'
284
+ task flag do |_, args|
285
+ outdated flag, args.to_a
286
+ end
217
287
  when 'build'
288
+ case flag
289
+ when :poetry
290
+ next unless build_backend == 'poetry.core.masonry.api'
291
+ when :pdm
292
+ next unless build_backend == 'pdm.backend'
293
+ when :hatch
294
+ next unless build_backend == 'hatchling.build'
295
+ end
218
296
  format_desc(action, flag, 'opts*', after: case flag
219
297
  when :python then 'srcdir?'
298
+ when :poetry then 'output?'
299
+ when :pdm then 'dest?'
220
300
  when :hatch then 'location?'
221
301
  end)
222
302
  task flag do |_, args|
223
303
  build! flag, args.to_a
224
304
  end
305
+ break unless flag == :python
225
306
  when 'publish'
226
307
  format_desc(action, flag, 'opts*', after: case flag
227
308
  when :hatch then 'artifacts?'
@@ -269,7 +350,7 @@ module Squared
269
350
  end
270
351
  end
271
352
 
272
- def outdated(*, sync: invoked_sync?('outdated'))
353
+ def outdated(flag = nil, opts = [], sync: invoked_sync?('outdated'))
273
354
  cmd = pip_session 'list', '--outdated'
274
355
  append_global
275
356
  cmd = session_done cmd
@@ -279,28 +360,35 @@ module Squared
279
360
  print_item banner if sync
280
361
  start = 0
281
362
  found = 0
282
- major = 0
363
+ major = []
364
+ minor = []
365
+ patch = []
283
366
  pwd_set(from: :outdated) do
284
367
  buffer = []
285
368
  out = ->(val) { sync ? puts(val) : buffer << val }
286
369
  IO.popen(runenv || {}, cmd).each do |line|
287
- next if line.match?(/^[\s-]+$/)
370
+ next if line.match?(/^[ -]+$/)
288
371
 
289
372
  if start > 0
290
373
  unless stdin?
291
- data = line.scan(SEM_VER)
292
- next unless (cur = data.shift) && (lat = data.shift)
374
+ cur, lat = line.scan(SEM_VER)
375
+ next unless cur && lat
293
376
 
294
377
  latest = lat.join
295
378
  current = cur.join
296
379
  semver cur
297
380
  semver lat
298
- if semmajor?(cur, lat)
299
- type = 2
300
- major += 1
301
- else
302
- type = cur[2] == lat[2] ? 0 : 1
303
- end
381
+ name = line.split(' ', 2).first
382
+ type = if semmajor?(cur, lat)
383
+ major << name
384
+ 2
385
+ elsif cur[2] == lat[2]
386
+ patch << name
387
+ 0
388
+ else
389
+ minor << name
390
+ 1
391
+ end
304
392
  if type == 0
305
393
  styles = color(:yellow)
306
394
  else
@@ -334,7 +422,18 @@ module Squared
334
422
  puts buffer
335
423
  end
336
424
  if found > 0
337
- puts print_footer empty_status('Updates are available', 'major', major)
425
+ print_status(major.size, minor.size, patch.size, from: :outdated)
426
+ pkg = case flag
427
+ when :major
428
+ major + minor + patch
429
+ when :minor
430
+ minor + patch
431
+ when :patch
432
+ patch
433
+ end
434
+ unless !pkg || pkg.empty?
435
+ install(:upgrade, pkg, strategy: opts.include?('eager') ? 'eager' : nil, user: opts.include?('user'))
436
+ end
338
437
  elsif start == 0
339
438
  puts 'No updates were found'
340
439
  end
@@ -342,7 +441,7 @@ module Squared
342
441
  on :last, :outdated
343
442
  end
344
443
 
345
- def install(flag, opts = [], strategy: nil)
444
+ def install(flag, opts = [], strategy: nil, user: nil)
346
445
  cmd = pip_session 'install'
347
446
  out = append_pip(flag, opts, from: :install)
348
447
  case flag
@@ -352,6 +451,7 @@ module Squared
352
451
  when :upgrade
353
452
  raise_error('no packages listed', hint: flag) if out.empty?
354
453
  cmd << '--upgrade'
454
+ cmd << '--user' if user
355
455
  cmd << basic_option('upgrade-strategy', strategy) if strategy
356
456
  append_value out
357
457
  end
@@ -366,6 +466,9 @@ module Squared
366
466
  when :poetry
367
467
  cmd = poetry_session 'build'
368
468
  list = OPT_POETRY[:build] + OPT_POETRY[:common]
469
+ when :pdm
470
+ cmd, opts = pdm_session('build', opts: opts)
471
+ list = OPT_PDM[:build]
369
472
  when :hatch
370
473
  cmd, opts = hatch_session('build', opts: opts)
371
474
  list = OPT_HATCH[:build]
@@ -373,7 +476,7 @@ module Squared
373
476
  srcdir = nil
374
477
  op = OptionPartition.new(opts, list, cmd, project: self, single: singleopt(flag))
375
478
  op.each do |opt|
376
- if !srcdir && basepath(opt).exist? && projectpath?(opt)
479
+ if !srcdir && basepath(opt.chomp('*')).exist? && projectpath?(opt.chomp('*'))
377
480
  srcdir = opt
378
481
  else
379
482
  op.found << opt
@@ -381,12 +484,13 @@ module Squared
381
484
  end
382
485
  op.swap
383
486
  case flag
384
- when :poetry
487
+ when :poetry, :pdm
385
488
  if srcdir
386
- if op.arg?('o', 'output')
489
+ args = flag == :pdm ? ['d', 'dest'] : ['o', 'output']
490
+ if op.arg?(*args)
387
491
  op.extras << srcdir
388
492
  else
389
- op << quote_option('output', path + srcdir)
493
+ op << quote_option(args.last, path + srcdir)
390
494
  end
391
495
  srcdir = nil
392
496
  end
@@ -408,21 +512,35 @@ module Squared
408
512
  when :poetry
409
513
  poetry_session 'publish'
410
514
  list = OPT_POETRY[:publish] + OPT_POETRY[:common]
411
- when :twine
412
- session 'twine', 'upload'
413
- list = OPT_TWINE[:publish]
515
+ when :pdm
516
+ opts = pdm_session('publish', opts: opts).last
517
+ list = OPT_PDM[:publish]
414
518
  when :hatch
415
519
  opts = hatch_session('publish', opts: opts).last
416
520
  list = OPT_HATCH[:publish]
521
+ when :twine
522
+ session 'twine', 'upload'
523
+ list = OPT_TWINE[:publish]
417
524
  end
418
525
  op = OptionPartition.new(opts, list, @session, project: self, single: singleopt(flag))
419
- if op.empty?
420
- dist = path + 'dist'
421
- raise_error('no source files found', hint: dist) unless dist.directory? && !dist.empty?
422
- op.extras << "#{dist}/*" unless flag == :poetry
526
+ dist = lambda do
527
+ (path + 'dist').tap do |dir|
528
+ raise_error('no source files found', hint: dir) unless dir.directory? && !dir.empty?
529
+ end
423
530
  end
424
- op.append
425
- run(from: :"#{flag}:publish")
531
+ case flag
532
+ when :hatch, :twine
533
+ if op.empty?
534
+ op.extras << "#{dist.call}/*"
535
+ else
536
+ op.map! { |val| path + val }
537
+ end
538
+ op.append
539
+ else
540
+ dist.call unless op.arg?(*(flag == :poetry ? ['dist-dir'] : ['d', 'dest']))
541
+ op.clear(pass: false)
542
+ end
543
+ run(from: :"#{flag}:publish", interactive: "Publish #{sub_style(project, styles: theme[:active])}")
426
544
  end
427
545
 
428
546
  def pip(flag, opts = [])
@@ -491,11 +609,19 @@ module Squared
491
609
  ret
492
610
  end
493
611
 
612
+ def pdm_session(*cmd, opts: nil)
613
+ create_session(*cmd, name: 'pdm', common: OPT_PDM[:common], opts: opts)
614
+ end
615
+
494
616
  def hatch_session(*cmd, opts: nil)
495
- return session('hatch', *preopts, *cmd, path: venv.nil?) unless opts
617
+ create_session(*cmd, name: 'hatch', common: OPT_HATCH[:common], opts: opts)
618
+ end
619
+
620
+ def create_session(*cmd, name:, common:, opts: nil)
621
+ return session(name, *preopts, *cmd, path: venv.nil?) unless opts
496
622
 
497
- op = OptionPartition.new(opts, OPT_HATCH[:common], project: self, single: singleopt)
498
- ret = session('hatch', *op.to_a, *cmd, path: venv.nil?)
623
+ op = OptionPartition.new(opts, common, project: self, single: singleopt)
624
+ ret = session(name, *op.to_a, *cmd, path: venv.nil?)
499
625
  [ret, op.extras]
500
626
  end
501
627
 
@@ -573,13 +699,77 @@ module Squared
573
699
  append_nocolor(target: target)
574
700
  end
575
701
 
576
- def editable_set(val)
577
- @editable = case val
578
- when '.', Pathname
579
- val
580
- when String
581
- Pathname.new(editable)
702
+ def build_backend
703
+ @build_backend ||= read_pyproject('build-system', 'build-backend') || ''
704
+ end
705
+
706
+ def read_pyproject(table, key = nil)
707
+ return [] unless (file = pyprojectfile)
708
+
709
+ unless (ret = (@pyproject ||= {})[table])
710
+ ret = []
711
+ start = /^\s*\[#{Regexp.escape(table)}\]\s*$/
712
+ ch = nil
713
+ found = false
714
+ File.foreach(file) do |line|
715
+ if found
716
+ break if line.match?(/^\s*\[[\w.-]+\]\s*$/)
717
+
718
+ if ch
719
+ val = line.rstrip
720
+ case ch
721
+ when '}', ']'
722
+ ch = nil if val.end_with?(ch)
723
+ val = "\n#{val}"
724
+ else
725
+ if val.chomp!(ch)
726
+ ch = nil
727
+ else
728
+ val = line
729
+ end
730
+ end
731
+ ret.last[1] += val
732
+ elsif (data = line.match(/^\s*(\S+)\s*=\s*([+-]?[\d.]+|true|false|("""|'''|["'\[{])(.*?))\s*$/))
733
+ if (val = data[4])
734
+ case (ch = data[3])
735
+ when '{', '['
736
+ val = "#{ch}#{val}"
737
+ ch = ch == '{' ? '}' : ']'
738
+ ch = nil if val.end_with?(ch)
739
+ else
740
+ if val.chomp!(ch)
741
+ ch = nil
742
+ elsif ch.size == 1
743
+ next
582
744
  end
745
+ end
746
+ else
747
+ val = case (val = data[2])
748
+ when 'true'
749
+ true
750
+ when 'false'
751
+ false
752
+ else
753
+ val.include?('.') ? val.to_f : val.to_i
754
+ end
755
+ end
756
+ ret << [data[1], val]
757
+ end
758
+ else
759
+ found = line.match?(start)
760
+ end
761
+ end
762
+ @pyproject[table] = ret
763
+ end
764
+ return ret.find { |val| val[0] == key }&.last if key
765
+
766
+ ret
767
+ end
768
+
769
+ def pyprojectfile
770
+ return unless (ret = basepath(DEP_PYTHON[2])).exist?
771
+
772
+ ret
583
773
  end
584
774
 
585
775
  def singleopt(flag = nil)
@@ -622,7 +812,18 @@ module Squared
622
812
  @venv&.join(workspace.windows? ? 'Scripts' : 'bin')
623
813
  end
624
814
 
815
+ def editable_set(val)
816
+ @editable = case val
817
+ when '.', Pathname
818
+ val
819
+ when String
820
+ Pathname.new(editable)
821
+ end
822
+ end
823
+
625
824
  def venv_set(val)
825
+ return unless val
826
+
626
827
  if val.is_a?(Array)
627
828
  val, *opts = val
628
829
  @venvopts = opts