squared 0.5.0 → 0.5.2

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 = [])
@@ -495,11 +613,19 @@ module Squared
495
613
  ret
496
614
  end
497
615
 
616
+ def pdm_session(*cmd, opts: nil)
617
+ create_session(*cmd, name: 'pdm', common: OPT_PDM[:common], opts: opts)
618
+ end
619
+
498
620
  def hatch_session(*cmd, opts: nil)
499
- return session('hatch', *preopts, *cmd, path: venv.nil?) unless opts
621
+ create_session(*cmd, name: 'hatch', common: OPT_HATCH[:common], opts: opts)
622
+ end
623
+
624
+ def create_session(*cmd, name:, common:, opts: nil)
625
+ return session(name, *preopts, *cmd, path: venv.nil?) unless opts
500
626
 
501
- op = OptionPartition.new(opts, OPT_HATCH[:common], project: self, single: singleopt)
502
- ret = session('hatch', *op.to_a, *cmd, path: venv.nil?)
627
+ op = OptionPartition.new(opts, common, project: self, single: singleopt)
628
+ ret = session(name, *op.to_a, *cmd, path: venv.nil?)
503
629
  [ret, op.extras]
504
630
  end
505
631
 
@@ -577,13 +703,77 @@ module Squared
577
703
  append_nocolor(target: target)
578
704
  end
579
705
 
580
- def editable_set(val)
581
- @editable = case val
582
- when '.', Pathname
583
- val
584
- when String
585
- Pathname.new(editable)
706
+ def build_backend
707
+ @build_backend ||= read_pyproject('build-system', 'build-backend') || ''
708
+ end
709
+
710
+ def read_pyproject(table, key = nil)
711
+ return [] unless (file = pyprojectfile)
712
+
713
+ unless (ret = (@pyproject ||= {})[table])
714
+ ret = []
715
+ start = /^\s*\[#{Regexp.escape(table)}\]\s*$/
716
+ ch = nil
717
+ found = false
718
+ File.foreach(file) do |line|
719
+ if found
720
+ break if line.match?(/^\s*\[[\w.-]+\]\s*$/)
721
+
722
+ if ch
723
+ val = line.rstrip
724
+ case ch
725
+ when '}', ']'
726
+ ch = nil if val.end_with?(ch)
727
+ val = "\n#{val}"
728
+ else
729
+ if val.chomp!(ch)
730
+ ch = nil
731
+ else
732
+ val = line
733
+ end
734
+ end
735
+ ret.last[1] += val
736
+ elsif (data = line.match(/^\s*(\S+)\s*=\s*([+-]?[\d.]+|true|false|("""|'''|["'\[{])(.*?))\s*$/))
737
+ if (val = data[4])
738
+ case (ch = data[3])
739
+ when '{', '['
740
+ val = "#{ch}#{val}"
741
+ ch = ch == '{' ? '}' : ']'
742
+ ch = nil if val.end_with?(ch)
743
+ else
744
+ if val.chomp!(ch)
745
+ ch = nil
746
+ elsif ch.size == 1
747
+ next
586
748
  end
749
+ end
750
+ else
751
+ val = case (val = data[2])
752
+ when 'true'
753
+ true
754
+ when 'false'
755
+ false
756
+ else
757
+ val.include?('.') ? val.to_f : val.to_i
758
+ end
759
+ end
760
+ ret << [data[1], val]
761
+ end
762
+ else
763
+ found = line.match?(start)
764
+ end
765
+ end
766
+ @pyproject[table] = ret
767
+ end
768
+ return ret.find { |val| val[0] == key }&.last if key
769
+
770
+ ret
771
+ end
772
+
773
+ def pyprojectfile
774
+ return unless (ret = basepath(DEP_PYTHON[2])).exist?
775
+
776
+ ret
587
777
  end
588
778
 
589
779
  def singleopt(flag = nil)
@@ -626,7 +816,18 @@ module Squared
626
816
  @venv&.join(workspace.windows? ? 'Scripts' : 'bin')
627
817
  end
628
818
 
819
+ def editable_set(val)
820
+ @editable = case val
821
+ when '.', Pathname
822
+ val
823
+ when String
824
+ Pathname.new(editable)
825
+ end
826
+ end
827
+
629
828
  def venv_set(val)
829
+ return unless val
830
+
630
831
  if val.is_a?(Array)
631
832
  val, *opts = val
632
833
  @venvopts = opts