squared 0.4.12 → 0.4.14

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,24 +87,21 @@ module Squared
80
87
  initialize_build(Python.ref, **kwargs)
81
88
  initialize_env(**kwargs)
82
89
  end
83
- @dependindex = DEP_PYTHON.index { |file| basepath(file).exist? }
84
- @dependfile = @path + DEP_PYTHON[@dependindex || 0]
85
90
  @verbose = verbose.size if verbose.is_a?(String) && verbose.match?(/\Av+\z/)
86
- @editable = case editable
87
- when '.', Pathname
88
- editable
89
- when String
90
- Pathname.new(editable)
91
- end
92
- venv_set venv if venv
91
+ dependfile_set DEP_PYTHON
92
+ editable_set editable
93
+ venv_set kwargs[:venv]
93
94
  end
94
95
 
95
96
  subtasks({
96
- 'venv' => %i[run create remove show].freeze,
97
+ 'venv' => %i[exec create remove show].freeze,
97
98
  'pip' => %i[uninstall freeze].freeze,
98
99
  'install' => %i[user force upgrade target editable].freeze,
99
- 'build' => %i[python poetry hatch].freeze,
100
- '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,
104
+ 'exec' => nil
101
105
  })
102
106
 
103
107
  def ref
@@ -112,105 +116,202 @@ module Squared
112
116
  Python.subtasks do |action, flags|
113
117
  next if @pass.include?(action)
114
118
 
115
- namespace action do
116
- flags.each do |flag|
117
- case action
118
- when 'venv'
119
- if flag == :create
120
- format_desc action, flag, 'dir,opts*'
121
- task flag, [:dir] do |_, args|
122
- dir = path + param_guard(action, flag, args: args, key: :dir)
123
- venv_create dir, args.extras
119
+ if flags.nil?
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
124
168
  end
125
- elsif venv
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
177
+ when 'exec'
178
+ format_desc action, nil, 'command|:,args*'
179
+ task action do |_, args|
180
+ i = (args = args.to_a).delete(':')
181
+ cmd = if i && !workspace.windows?
182
+ readline('Enter script', force: true, multiline: ['##', ';'])
183
+ elsif i || args.empty?
184
+ readline('Enter command', force: true)
185
+ else
186
+ if (val = command_args(args, prefix: 'python'))
187
+ args << val
188
+ end
189
+ args.join(' ')
190
+ end
191
+ shell(cmd, name: :exec, chdir: path)
192
+ end
193
+ end
194
+ else
195
+ namespace action do
196
+ flags.each do |flag|
197
+ case action
198
+ when 'venv'
199
+ if flag == :create
200
+ format_desc action, flag, 'dir,opts*'
201
+ task flag, [:dir] do |_, args|
202
+ dir = path + param_guard(action, flag, args: args, key: :dir)
203
+ venv_create dir, args.extras
204
+ end
205
+ elsif venv
206
+ case flag
207
+ when :remove
208
+ next unless projectpath?(venv)
209
+
210
+ format_desc action, flag, 'c|create?,d|depend?'
211
+ task flag do |_, args|
212
+ rm_rf(venv, verbose: true)
213
+ venv_init if has_value?(%w[c create], args.to_a)
214
+ depend if has_value?(%w[d depend], args.to_a)
215
+ end
216
+ when :exec
217
+ format_desc action, flag, 'command,args*'
218
+ task flag do |_, args|
219
+ args = args.to_a
220
+ if args.empty?
221
+ args = readline('Enter command', force: true).split(' ', 2)
222
+ elsif args.size == 1 && !option('interactive', prefix: 'venv', equals: '0')
223
+ args << readline('Enter arguments', force: false)
224
+ end
225
+ venv_init
226
+ run args.join(' ')
227
+ end
228
+ when :show
229
+ format_desc action, flag
230
+ task flag do
231
+ puts venv
232
+ end
233
+ end
234
+ end
235
+ when 'pip'
126
236
  case flag
127
- when :remove
128
- next unless projectpath?(venv)
129
-
130
- format_desc action, flag, 'c|create?,d|depend?'
237
+ when :freeze
238
+ format_desc action, flag, "file?=#{DEP_PYTHON[4]},opts*"
131
239
  task flag do |_, args|
132
- rm_rf(venv, verbose: true)
133
- venv_init if has_value?(%w[c create], args.to_a)
134
- depend if has_value?(%w[d depend], args.to_a)
240
+ if (file = pip(flag, args.to_a)) && verbose
241
+ puts File.read(file)
242
+ end
135
243
  end
136
- when :run
137
- format_desc action, flag, 'args+'
244
+ when :uninstall
245
+ format_desc action, flag, 'package+,opts*'
138
246
  task flag do |_, args|
139
- args = args.to_a
140
- args = readline('Enter command', force: true).split(' ', 2) if args.empty?
141
- venv_init
142
- run session(*args, path: false)
143
- end
144
- when :show
145
- format_desc action, flag
146
- task flag do
147
- puts venv
247
+ pip flag, args.to_a
148
248
  end
149
249
  end
150
- end
151
- when 'pip'
152
- case flag
153
- when :freeze
154
- format_desc action, flag, "file?=#{DEP_PYTHON[4]},opts*"
155
- task flag do |_, args|
156
- if (file = pip(flag, args.to_a)) && verbose
157
- puts File.read(file)
250
+ when 'install'
251
+ format_desc(action, flag, 'opts*', before: case flag
252
+ when :target then 'dir'
253
+ when :editable then 'path/url?'
254
+ when :upgrade then 'strategy?,package+'
255
+ end)
256
+ case flag
257
+ when :editable
258
+ task flag do |_, args|
259
+ install flag, args.to_a
260
+ end
261
+ when :upgrade
262
+ task flag, [:strategy] do |_, args|
263
+ case (strategy = args.strategy)
264
+ when 'eager', 'only-if-needed'
265
+ args = args.extras
266
+ else
267
+ args = args.to_a
268
+ strategy = nil
269
+ end
270
+ install(flag, args, strategy: strategy)
271
+ end
272
+ when :target
273
+ task flag, [:dir] do |_, args|
274
+ dir = param_guard(action, flag, args: args, key: :dir)
275
+ depend(flag, args.extras, target: dir)
276
+ end
277
+ else
278
+ task flag do |_, args|
279
+ depend flag, args.to_a
158
280
  end
159
281
  end
160
- when :uninstall
161
- format_desc action, flag, 'package+,opts*'
162
- task flag do |_, args|
163
- pip flag, args.to_a
164
- end
165
- end
166
- when 'install'
167
- format_desc(action, flag, 'opts*', before: case flag
168
- when :target then 'dir'
169
- when :editable then 'path/url?'
170
- when :upgrade then 'strategy?,package+'
171
- end)
172
- case flag
173
- when :editable
282
+ when 'outdated'
283
+ format_desc action, flag, 'eager?'
174
284
  task flag do |_, args|
175
- install flag, args.to_a
285
+ outdated flag, args.to_a
176
286
  end
177
- when :upgrade
178
- task flag, [:strategy] do |_, args|
179
- case (strategy = args.strategy)
180
- when 'eager', 'only-if-needed'
181
- args = args.extras
182
- else
183
- args = args.to_a
184
- strategy = nil
185
- end
186
- install(flag, args, strategy: strategy)
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'
187
295
  end
188
- when :target
189
- task flag, [:dir] do |_, args|
190
- dir = param_guard(action, flag, args: args, key: :dir)
191
- depend(flag, args.extras, target: dir)
296
+ format_desc(action, flag, 'opts*', after: case flag
297
+ when :python then 'srcdir?'
298
+ when :poetry then 'output?'
299
+ when :pdm then 'dest?'
300
+ when :hatch then 'location?'
301
+ end)
302
+ task flag do |_, args|
303
+ build! flag, args.to_a
192
304
  end
193
- else
305
+ break unless flag == :python
306
+ when 'publish'
307
+ format_desc(action, flag, 'opts*', after: case flag
308
+ when :hatch then 'artifacts?'
309
+ when :twine then 'dist?'
310
+ end)
194
311
  task flag do |_, args|
195
- depend flag, args.to_a
312
+ publish flag, args.to_a
196
313
  end
197
314
  end
198
- when 'build'
199
- format_desc(action, flag, 'opts*', after: case flag
200
- when :python then 'srcdir?'
201
- when :hatch then 'location?'
202
- end)
203
- task flag do |_, args|
204
- build! flag, args.to_a
205
- end
206
- when 'publish'
207
- format_desc(action, flag, 'opts*', after: case flag
208
- when :hatch then 'artifacts?'
209
- when :twine then 'dist?'
210
- end)
211
- task flag do |_, args|
212
- publish flag, args.to_a
213
- end
214
315
  end
215
316
  end
216
317
  end
@@ -249,7 +350,7 @@ module Squared
249
350
  end
250
351
  end
251
352
 
252
- def outdated(*, sync: invoked_sync?('outdated'))
353
+ def outdated(flag = nil, opts = [], sync: invoked_sync?('outdated'))
253
354
  cmd = pip_session 'list', '--outdated'
254
355
  append_global
255
356
  cmd = session_done cmd
@@ -259,28 +360,35 @@ module Squared
259
360
  print_item banner if sync
260
361
  start = 0
261
362
  found = 0
262
- major = 0
363
+ major = []
364
+ minor = []
365
+ patch = []
263
366
  pwd_set(from: :outdated) do
264
367
  buffer = []
265
368
  out = ->(val) { sync ? puts(val) : buffer << val }
266
369
  IO.popen(runenv || {}, cmd).each do |line|
267
- next if line.match?(/^[\s-]+$/)
370
+ next if line.match?(/^[ -]+$/)
268
371
 
269
372
  if start > 0
270
373
  unless stdin?
271
- data = line.scan(SEM_VER)
272
- next unless (cur = data.shift) && (lat = data.shift)
374
+ cur, lat = line.scan(SEM_VER)
375
+ next unless cur && lat
273
376
 
274
377
  latest = lat.join
275
378
  current = cur.join
276
379
  semver cur
277
380
  semver lat
278
- if semmajor?(cur, lat)
279
- type = 2
280
- major += 1
281
- else
282
- type = cur[2] == lat[2] ? 0 : 1
283
- 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
284
392
  if type == 0
285
393
  styles = color(:yellow)
286
394
  else
@@ -314,7 +422,16 @@ module Squared
314
422
  puts buffer
315
423
  end
316
424
  if found > 0
317
- 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?
318
435
  elsif start == 0
319
436
  puts 'No updates were found'
320
437
  end
@@ -346,6 +463,9 @@ module Squared
346
463
  when :poetry
347
464
  cmd = poetry_session 'build'
348
465
  list = OPT_POETRY[:build] + OPT_POETRY[:common]
466
+ when :pdm
467
+ cmd, opts = pdm_session('build', opts: opts)
468
+ list = OPT_PDM[:build]
349
469
  when :hatch
350
470
  cmd, opts = hatch_session('build', opts: opts)
351
471
  list = OPT_HATCH[:build]
@@ -353,7 +473,7 @@ module Squared
353
473
  srcdir = nil
354
474
  op = OptionPartition.new(opts, list, cmd, project: self, single: singleopt(flag))
355
475
  op.each do |opt|
356
- if !srcdir && basepath(opt).exist? && projectpath?(opt)
476
+ if !srcdir && basepath(opt.chomp('*')).exist? && projectpath?(opt.chomp('*'))
357
477
  srcdir = opt
358
478
  else
359
479
  op.found << opt
@@ -361,12 +481,13 @@ module Squared
361
481
  end
362
482
  op.swap
363
483
  case flag
364
- when :poetry
484
+ when :poetry, :pdm
365
485
  if srcdir
366
- if op.arg?('o', 'output')
486
+ args = flag == :pdm ? ['d', 'dest'] : ['o', 'output']
487
+ if op.arg?(*args)
367
488
  op.extras << srcdir
368
489
  else
369
- op << quote_option('output', path + srcdir)
490
+ op << quote_option(args.last, path + srcdir)
370
491
  end
371
492
  srcdir = nil
372
493
  end
@@ -388,21 +509,35 @@ module Squared
388
509
  when :poetry
389
510
  poetry_session 'publish'
390
511
  list = OPT_POETRY[:publish] + OPT_POETRY[:common]
391
- when :twine
392
- session 'twine', 'upload'
393
- list = OPT_TWINE[:publish]
512
+ when :pdm
513
+ opts = pdm_session('publish', opts: opts).last
514
+ list = OPT_PDM[:publish]
394
515
  when :hatch
395
516
  opts = hatch_session('publish', opts: opts).last
396
517
  list = OPT_HATCH[:publish]
518
+ when :twine
519
+ session 'twine', 'upload'
520
+ list = OPT_TWINE[:publish]
397
521
  end
398
522
  op = OptionPartition.new(opts, list, @session, project: self, single: singleopt(flag))
399
- if op.empty?
400
- dist = path + 'dist'
401
- raise_error('no source files found', hint: dist) unless dist.directory? && !dist.empty?
402
- 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
527
+ end
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)
403
539
  end
404
- op.append
405
- run(from: :"#{flag}:publish")
540
+ run(from: :"#{flag}:publish", interactive: "Publish #{sub_style(project, styles: theme[:active])}")
406
541
  end
407
542
 
408
543
  def pip(flag, opts = [])
@@ -432,7 +567,9 @@ module Squared
432
567
  else
433
568
  log.warn "variable_set: @#{key}=#{req} (not supported)"
434
569
  end
435
- when :venv, :editable
570
+ when :editable
571
+ editable_set val.first
572
+ when :venv
436
573
  instance_variable_set(:"@#{key}", val.empty? ? nil : basepath(*val))
437
574
  else
438
575
  super
@@ -469,11 +606,19 @@ module Squared
469
606
  ret
470
607
  end
471
608
 
609
+ def pdm_session(*cmd, opts: nil)
610
+ create_session(*cmd, name: 'pdm', common: OPT_PDM[:common], opts: opts)
611
+ end
612
+
472
613
  def hatch_session(*cmd, opts: nil)
473
- return session('hatch', *preopts, *cmd, path: venv.nil?) unless opts
614
+ create_session(*cmd, name: 'hatch', common: OPT_HATCH[:common], opts: opts)
615
+ end
616
+
617
+ def create_session(*cmd, name:, common:, opts: nil)
618
+ return session(name, *preopts, *cmd, path: venv.nil?) unless opts
474
619
 
475
- op = OptionPartition.new(opts, OPT_HATCH[:common], project: self, single: singleopt)
476
- ret = session('hatch', *op.to_a, *cmd, path: venv.nil?)
620
+ op = OptionPartition.new(opts, common, project: self, single: singleopt)
621
+ ret = session(name, *op.to_a, *cmd, path: venv.nil?)
477
622
  [ret, op.extras]
478
623
  end
479
624
 
@@ -551,6 +696,79 @@ module Squared
551
696
  append_nocolor(target: target)
552
697
  end
553
698
 
699
+ def build_backend
700
+ @build_backend ||= read_pyproject('build-system', 'build-backend') || ''
701
+ end
702
+
703
+ def read_pyproject(table, key = nil)
704
+ return [] unless (file = pyprojectfile)
705
+
706
+ unless (ret = (@pyproject ||= {})[table])
707
+ ret = []
708
+ start = /^\s*\[#{Regexp.escape(table)}\]\s*$/
709
+ ch = nil
710
+ found = false
711
+ File.foreach(file) do |line|
712
+ if found
713
+ break if line.match?(/^\s*\[[\w.-]+\]\s*$/)
714
+
715
+ if ch
716
+ val = line.rstrip
717
+ case ch
718
+ when '}', ']'
719
+ ch = nil if val.end_with?(ch)
720
+ val = "\n#{val}"
721
+ else
722
+ if val.chomp!(ch)
723
+ ch = nil
724
+ else
725
+ val = line
726
+ end
727
+ end
728
+ ret.last[1] += val
729
+ elsif (data = line.match(/^\s*(\S+)\s*=\s*([+-]?[\d.]+|true|false|("""|'''|["'\[{])(.*?))\s*$/))
730
+ if (val = data[4])
731
+ case (ch = data[3])
732
+ when '{', '['
733
+ val = "#{ch}#{val}"
734
+ ch = ch == '{' ? '}' : ']'
735
+ ch = nil if val.end_with?(ch)
736
+ else
737
+ if val.chomp!(ch)
738
+ ch = nil
739
+ elsif ch.size == 1
740
+ next
741
+ end
742
+ end
743
+ else
744
+ val = case (val = data[2])
745
+ when 'true'
746
+ true
747
+ when 'false'
748
+ false
749
+ else
750
+ val.include?('.') ? val.to_f : val.to_i
751
+ end
752
+ end
753
+ ret << [data[1], val]
754
+ end
755
+ else
756
+ found = line.match?(start)
757
+ end
758
+ end
759
+ @pyproject[table] = ret
760
+ end
761
+ return ret.find { |val| val[0] == key }&.last if key
762
+
763
+ ret
764
+ end
765
+
766
+ def pyprojectfile
767
+ return unless (ret = basepath(DEP_PYTHON[2])).exist?
768
+
769
+ ret
770
+ end
771
+
554
772
  def singleopt(flag = nil)
555
773
  case flag
556
774
  when :python
@@ -591,7 +809,18 @@ module Squared
591
809
  @venv&.join(workspace.windows? ? 'Scripts' : 'bin')
592
810
  end
593
811
 
812
+ def editable_set(val)
813
+ @editable = case val
814
+ when '.', Pathname
815
+ val
816
+ when String
817
+ Pathname.new(editable)
818
+ end
819
+ end
820
+
594
821
  def venv_set(val)
822
+ return unless val
823
+
595
824
  if val.is_a?(Array)
596
825
  val, *opts = val
597
826
  @venvopts = opts