squared 0.5.0 → 0.5.1

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?'
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,16 @@ 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
+ install(:upgrade, pkg, strategy: opts.include?('eager') ? 'eager' : nil) unless !pkg || pkg.empty?
338
435
  elsif start == 0
339
436
  puts 'No updates were found'
340
437
  end
@@ -366,6 +463,9 @@ module Squared
366
463
  when :poetry
367
464
  cmd = poetry_session 'build'
368
465
  list = OPT_POETRY[:build] + OPT_POETRY[:common]
466
+ when :pdm
467
+ cmd, opts = pdm_session('build', opts: opts)
468
+ list = OPT_PDM[:build]
369
469
  when :hatch
370
470
  cmd, opts = hatch_session('build', opts: opts)
371
471
  list = OPT_HATCH[:build]
@@ -373,7 +473,7 @@ module Squared
373
473
  srcdir = nil
374
474
  op = OptionPartition.new(opts, list, cmd, project: self, single: singleopt(flag))
375
475
  op.each do |opt|
376
- if !srcdir && basepath(opt).exist? && projectpath?(opt)
476
+ if !srcdir && basepath(opt.chomp('*')).exist? && projectpath?(opt.chomp('*'))
377
477
  srcdir = opt
378
478
  else
379
479
  op.found << opt
@@ -381,12 +481,13 @@ module Squared
381
481
  end
382
482
  op.swap
383
483
  case flag
384
- when :poetry
484
+ when :poetry, :pdm
385
485
  if srcdir
386
- if op.arg?('o', 'output')
486
+ args = flag == :pdm ? ['d', 'dest'] : ['o', 'output']
487
+ if op.arg?(*args)
387
488
  op.extras << srcdir
388
489
  else
389
- op << quote_option('output', path + srcdir)
490
+ op << quote_option(args.last, path + srcdir)
390
491
  end
391
492
  srcdir = nil
392
493
  end
@@ -408,21 +509,35 @@ module Squared
408
509
  when :poetry
409
510
  poetry_session 'publish'
410
511
  list = OPT_POETRY[:publish] + OPT_POETRY[:common]
411
- when :twine
412
- session 'twine', 'upload'
413
- list = OPT_TWINE[:publish]
512
+ when :pdm
513
+ opts = pdm_session('publish', opts: opts).last
514
+ list = OPT_PDM[:publish]
414
515
  when :hatch
415
516
  opts = hatch_session('publish', opts: opts).last
416
517
  list = OPT_HATCH[:publish]
518
+ when :twine
519
+ session 'twine', 'upload'
520
+ list = OPT_TWINE[:publish]
417
521
  end
418
522
  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
523
+ dist = lambda do
524
+ (path + 'dist').tap do |dir|
525
+ raise_error('no source files found', hint: dir) unless dir.directory? && !dir.empty?
526
+ end
423
527
  end
424
- op.append
425
- run(from: :"#{flag}:publish")
528
+ case flag
529
+ when :hatch, :twine
530
+ if op.empty?
531
+ op.extras << "#{dist.call}/*"
532
+ else
533
+ op.map! { |val| path + val }
534
+ end
535
+ op.append
536
+ else
537
+ dist.call unless op.arg?(*(flag == :poetry ? ['dist-dir'] : ['d', 'dest']))
538
+ op.clear(pass: false)
539
+ end
540
+ run(from: :"#{flag}:publish", interactive: "Publish #{sub_style(project, styles: theme[:active])}")
426
541
  end
427
542
 
428
543
  def pip(flag, opts = [])
@@ -495,11 +610,19 @@ module Squared
495
610
  ret
496
611
  end
497
612
 
613
+ def pdm_session(*cmd, opts: nil)
614
+ create_session(*cmd, name: 'pdm', common: OPT_PDM[:common], opts: opts)
615
+ end
616
+
498
617
  def hatch_session(*cmd, opts: nil)
499
- return session('hatch', *preopts, *cmd, path: venv.nil?) unless opts
618
+ create_session(*cmd, name: 'hatch', common: OPT_HATCH[:common], opts: opts)
619
+ end
620
+
621
+ def create_session(*cmd, name:, common:, opts: nil)
622
+ return session(name, *preopts, *cmd, path: venv.nil?) unless opts
500
623
 
501
- op = OptionPartition.new(opts, OPT_HATCH[:common], project: self, single: singleopt)
502
- ret = session('hatch', *op.to_a, *cmd, path: venv.nil?)
624
+ op = OptionPartition.new(opts, common, project: self, single: singleopt)
625
+ ret = session(name, *op.to_a, *cmd, path: venv.nil?)
503
626
  [ret, op.extras]
504
627
  end
505
628
 
@@ -577,13 +700,77 @@ module Squared
577
700
  append_nocolor(target: target)
578
701
  end
579
702
 
580
- def editable_set(val)
581
- @editable = case val
582
- when '.', Pathname
583
- val
584
- when String
585
- Pathname.new(editable)
703
+ def build_backend
704
+ @build_backend ||= read_pyproject('build-system', 'build-backend') || ''
705
+ end
706
+
707
+ def read_pyproject(table, key = nil)
708
+ return [] unless (file = pyprojectfile)
709
+
710
+ unless (ret = (@pyproject ||= {})[table])
711
+ ret = []
712
+ start = /^\s*\[#{Regexp.escape(table)}\]\s*$/
713
+ ch = nil
714
+ found = false
715
+ File.foreach(file) do |line|
716
+ if found
717
+ break if line.match?(/^\s*\[[\w.-]+\]\s*$/)
718
+
719
+ if ch
720
+ val = line.rstrip
721
+ case ch
722
+ when '}', ']'
723
+ ch = nil if val.end_with?(ch)
724
+ val = "\n#{val}"
725
+ else
726
+ if val.chomp!(ch)
727
+ ch = nil
728
+ else
729
+ val = line
730
+ end
731
+ end
732
+ ret.last[1] += val
733
+ elsif (data = line.match(/^\s*(\S+)\s*=\s*([+-]?[\d.]+|true|false|("""|'''|["'\[{])(.*?))\s*$/))
734
+ if (val = data[4])
735
+ case (ch = data[3])
736
+ when '{', '['
737
+ val = "#{ch}#{val}"
738
+ ch = ch == '{' ? '}' : ']'
739
+ ch = nil if val.end_with?(ch)
740
+ else
741
+ if val.chomp!(ch)
742
+ ch = nil
743
+ elsif ch.size == 1
744
+ next
586
745
  end
746
+ end
747
+ else
748
+ val = case (val = data[2])
749
+ when 'true'
750
+ true
751
+ when 'false'
752
+ false
753
+ else
754
+ val.include?('.') ? val.to_f : val.to_i
755
+ end
756
+ end
757
+ ret << [data[1], val]
758
+ end
759
+ else
760
+ found = line.match?(start)
761
+ end
762
+ end
763
+ @pyproject[table] = ret
764
+ end
765
+ return ret.find { |val| val[0] == key }&.last if key
766
+
767
+ ret
768
+ end
769
+
770
+ def pyprojectfile
771
+ return unless (ret = basepath(DEP_PYTHON[2])).exist?
772
+
773
+ ret
587
774
  end
588
775
 
589
776
  def singleopt(flag = nil)
@@ -626,7 +813,18 @@ module Squared
626
813
  @venv&.join(workspace.windows? ? 'Scripts' : 'bin')
627
814
  end
628
815
 
816
+ def editable_set(val)
817
+ @editable = case val
818
+ when '.', Pathname
819
+ val
820
+ when String
821
+ Pathname.new(editable)
822
+ end
823
+ end
824
+
629
825
  def venv_set(val)
826
+ return unless val
827
+
630
828
  if val.is_a?(Array)
631
829
  val, *opts = val
632
830
  @venvopts = opts