dotted-notation 0.43.7__tar.gz → 0.43.8__tar.gz

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 (79) hide show
  1. {dotted_notation-0.43.7/dotted_notation.egg-info → dotted_notation-0.43.8}/PKG-INFO +31 -7
  2. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/README.md +30 -6
  3. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/sqlize.py +58 -23
  4. {dotted_notation-0.43.7 → dotted_notation-0.43.8/dotted_notation.egg-info}/PKG-INFO +31 -7
  5. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/setup.py +1 -1
  6. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_sqlize.py +39 -9
  7. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/LICENSE +0 -0
  8. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/__init__.py +0 -0
  9. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/__main__.py +0 -0
  10. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/access.py +0 -0
  11. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/api.py +0 -0
  12. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/base.py +0 -0
  13. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/cli/__init__.py +0 -0
  14. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/cli/_compat.py +0 -0
  15. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/cli/formats.py +0 -0
  16. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/cli/main.py +0 -0
  17. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/containers.py +0 -0
  18. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/engine.py +0 -0
  19. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/filters.py +0 -0
  20. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/grammar.py +0 -0
  21. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/groups.py +0 -0
  22. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/matchers.py +0 -0
  23. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/predicates.py +0 -0
  24. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/recursive.py +0 -0
  25. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/results.py +0 -0
  26. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/transforms.py +0 -0
  27. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/utils.py +0 -0
  28. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/utypes.py +0 -0
  29. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/wrappers.py +0 -0
  30. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted_notation.egg-info/SOURCES.txt +0 -0
  31. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted_notation.egg-info/dependency_links.txt +0 -0
  32. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted_notation.egg-info/entry_points.txt +0 -0
  33. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted_notation.egg-info/requires.txt +0 -0
  34. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted_notation.egg-info/top_level.txt +0 -0
  35. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/setup.cfg +0 -0
  36. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/__init__.py +0 -0
  37. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_api.py +0 -0
  38. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_appender.py +0 -0
  39. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_assemble.py +0 -0
  40. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_attrs.py +0 -0
  41. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_bindings.py +0 -0
  42. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_cli.py +0 -0
  43. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_concat.py +0 -0
  44. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_container_filter.py +0 -0
  45. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_cut.py +0 -0
  46. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_empty.py +0 -0
  47. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_filter_keyvalue.py +0 -0
  48. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_get.py +0 -0
  49. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_guard_transforms.py +0 -0
  50. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_invert.py +0 -0
  51. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_json_sentinels.py +0 -0
  52. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_keys_values.py +0 -0
  53. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_match.py +0 -0
  54. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_matchable.py +0 -0
  55. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_named_subst.py +0 -0
  56. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_negation.py +0 -0
  57. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_nop.py +0 -0
  58. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_numeric.py +0 -0
  59. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_opgroup.py +0 -0
  60. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_pluck.py +0 -0
  61. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_predicates.py +0 -0
  62. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_quote_idempotent.py +0 -0
  63. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_recursive.py +0 -0
  64. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_reference.py +0 -0
  65. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_replace.py +0 -0
  66. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_slice.py +0 -0
  67. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_softcut.py +0 -0
  68. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_strict.py +0 -0
  69. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_string_glob.py +0 -0
  70. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_subst_escape.py +0 -0
  71. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_subst_transforms.py +0 -0
  72. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_threading.py +0 -0
  73. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_transforms.py +0 -0
  74. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_translate.py +0 -0
  75. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_type_restriction.py +0 -0
  76. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_unpack.py +0 -0
  77. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_update.py +0 -0
  78. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_update_if.py +0 -0
  79. {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_value_guard.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.43.7
3
+ Version: 0.43.8
4
4
  Summary: Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms
5
5
  Home-page: https://github.com/freywaid/dotted
6
6
  Author: Frey Waid
@@ -2977,10 +2977,26 @@ Not yet supported:
2977
2977
 
2978
2978
  ### Paramstyles
2979
2979
 
2980
- `Resolver.build(sql, paramstyle='named', **bindings)` defaults to
2981
- PEP 249 named style (`:name`). For Postgres-native `$N` (what asyncpg
2982
- and other raw PG drivers accept), pass `paramstyle='dollar-numeric'`
2983
- the output changes to positional:
2980
+ `Resolver.build(sql, paramstyle='named', **bindings)` renders the
2981
+ fragment in the chosen PEP 249 paramstyle. Supported:
2982
+
2983
+ | paramstyle | placeholder | output shape | typical driver |
2984
+ |-------------------|----------------|--------------|---------------------------|
2985
+ | `named` | `:name` | dict | SQLAlchemy `text`, sqlite3 |
2986
+ | `pyformat` | `%(name)s` | dict | psycopg (v2/v3) named |
2987
+ | `qmark` | `?` | list | sqlite3, ODBC |
2988
+ | `format` | `%s` | list | psycopg positional |
2989
+ | `numeric` | `:1, :2` | list | Oracle-style |
2990
+ | `dollar-numeric` | `$1, $2` | list | asyncpg, native Postgres |
2991
+
2992
+ The dict styles (`named`, `pyformat`) and the numbered positional
2993
+ styles (`numeric`, `dollar-numeric`) have **back-reference**: a
2994
+ substitution used in multiple places in the SQL shares a single bind
2995
+ entry. `qmark` and `format` have no back-reference — each occurrence
2996
+ consumes its own arg, so a repeated substitution emits its value
2997
+ multiple times.
2998
+
2999
+ Examples:
2984
3000
 
2985
3001
  >>> r = dotted.sqlize('(age >= 18 & status = "active")')
2986
3002
  >>> dotted.Resolver.build(r.where, paramstyle='dollar-numeric')
@@ -2990,8 +3006,16 @@ and other raw PG drivers accept), pass `paramstyle='dollar-numeric'`
2990
3006
  >>> dotted.Resolver.build(r.where, paramstyle='dollar-numeric', min_age=30)
2991
3007
  ('age >= $1', [30])
2992
3008
 
2993
- Under `'dollar-numeric'`, the second element is a list of positional
2994
- args ready for `await conn.execute(sql, *args)` asyncpg style.
3009
+ >>> dotted.Resolver.build(r.where, paramstyle='pyformat', min_age=30)
3010
+ ('age >= %(min_age)s', {'min_age': 30})
3011
+
3012
+ Back-reference vs no-backref on repeated substitutions:
3013
+
3014
+ >>> r = dotted.sqlize('(age >= $(x) & weight = $(x))')
3015
+ >>> dotted.Resolver.build(r.where, paramstyle='numeric', x=42)
3016
+ ('(age >= :1) AND (weight = :1)', [42])
3017
+ >>> dotted.Resolver.build(r.where, paramstyle='qmark', x=42)
3018
+ ('(age >= ?) AND (weight = ?)', [42, 42])
2995
3019
 
2996
3020
  A `ParamStyle` `StrEnum` is also available for autocomplete and type
2997
3021
  checking:
@@ -2940,10 +2940,26 @@ Not yet supported:
2940
2940
 
2941
2941
  ### Paramstyles
2942
2942
 
2943
- `Resolver.build(sql, paramstyle='named', **bindings)` defaults to
2944
- PEP 249 named style (`:name`). For Postgres-native `$N` (what asyncpg
2945
- and other raw PG drivers accept), pass `paramstyle='dollar-numeric'`
2946
- the output changes to positional:
2943
+ `Resolver.build(sql, paramstyle='named', **bindings)` renders the
2944
+ fragment in the chosen PEP 249 paramstyle. Supported:
2945
+
2946
+ | paramstyle | placeholder | output shape | typical driver |
2947
+ |-------------------|----------------|--------------|---------------------------|
2948
+ | `named` | `:name` | dict | SQLAlchemy `text`, sqlite3 |
2949
+ | `pyformat` | `%(name)s` | dict | psycopg (v2/v3) named |
2950
+ | `qmark` | `?` | list | sqlite3, ODBC |
2951
+ | `format` | `%s` | list | psycopg positional |
2952
+ | `numeric` | `:1, :2` | list | Oracle-style |
2953
+ | `dollar-numeric` | `$1, $2` | list | asyncpg, native Postgres |
2954
+
2955
+ The dict styles (`named`, `pyformat`) and the numbered positional
2956
+ styles (`numeric`, `dollar-numeric`) have **back-reference**: a
2957
+ substitution used in multiple places in the SQL shares a single bind
2958
+ entry. `qmark` and `format` have no back-reference — each occurrence
2959
+ consumes its own arg, so a repeated substitution emits its value
2960
+ multiple times.
2961
+
2962
+ Examples:
2947
2963
 
2948
2964
  >>> r = dotted.sqlize('(age >= 18 & status = "active")')
2949
2965
  >>> dotted.Resolver.build(r.where, paramstyle='dollar-numeric')
@@ -2953,8 +2969,16 @@ and other raw PG drivers accept), pass `paramstyle='dollar-numeric'`
2953
2969
  >>> dotted.Resolver.build(r.where, paramstyle='dollar-numeric', min_age=30)
2954
2970
  ('age >= $1', [30])
2955
2971
 
2956
- Under `'dollar-numeric'`, the second element is a list of positional
2957
- args ready for `await conn.execute(sql, *args)` asyncpg style.
2972
+ >>> dotted.Resolver.build(r.where, paramstyle='pyformat', min_age=30)
2973
+ ('age >= %(min_age)s', {'min_age': 30})
2974
+
2975
+ Back-reference vs no-backref on repeated substitutions:
2976
+
2977
+ >>> r = dotted.sqlize('(age >= $(x) & weight = $(x))')
2978
+ >>> dotted.Resolver.build(r.where, paramstyle='numeric', x=42)
2979
+ ('(age >= :1) AND (weight = :1)', [42])
2980
+ >>> dotted.Resolver.build(r.where, paramstyle='qmark', x=42)
2981
+ ('(age >= ?) AND (weight = ?)', [42, 42])
2958
2982
 
2959
2983
  A `ParamStyle` `StrEnum` is also available for autocomplete and type
2960
2984
  checking:
@@ -198,9 +198,22 @@ class ParamStyle(enum.StrEnum):
198
198
 
199
199
  `StrEnum` — members compare equal to their string values, so callers
200
200
  can pass either `ParamStyle.named` or `'named'`.
201
+
202
+ Output shape:
203
+ - `named` / `pyformat`: `(sql, params_dict)`. Repeated markers
204
+ share one binding entry (back-reference by name).
205
+ - `dollar-numeric` / `numeric`: `(sql, args_list)`. Repeated
206
+ markers share one position (back-reference by number).
207
+ - `qmark` / `format`: `(sql, args_list)`. Each marker occurrence
208
+ produces a separate arg — these styles have no back-reference
209
+ so a repeated substitution's value is emitted multiple times.
201
210
  """
202
- named = 'named' # :name (SQLAlchemy, sqlite3, etc.)
203
- dollar_numeric = 'dollar-numeric' # $1, $2 (asyncpg, native PG)
211
+ named = 'named' # :name (SQLAlchemy, sqlite3)
212
+ pyformat = 'pyformat' # %(name)s (psycopg)
213
+ qmark = 'qmark' # ? (sqlite3 positional)
214
+ format = 'format' # %s (psycopg positional)
215
+ numeric = 'numeric' # :1, :2 (Oracle-style)
216
+ dollar_numeric = 'dollar-numeric' # $1, $2 (asyncpg, native PG)
204
217
 
205
218
 
206
219
  _PARAMSTYLES = tuple(ps.value for ps in ParamStyle)
@@ -280,20 +293,23 @@ class SQLFragment:
280
293
  """
281
294
  Return self.text with every `{marker}` replaced by the return
282
295
  value of `replacer(marker_name)`. Text-level operation: the
283
- caller decides what placeholder form to emit (`:name`, `$N`, or
284
- anything else) and is responsible for tracking per-marker values
296
+ caller decides what placeholder form to emit (`:name`, `$N`,
297
+ `?`, etc.) and is responsible for tracking per-marker values
285
298
  externally. Literal braces in the fragment (doubled as `{{`/
286
- `}}`) are unescaped to single braces in the output, as per
287
- Python `str.format` rules.
299
+ `}}`) are unescaped to single braces in the output, per Python
300
+ format-string rules.
288
301
 
289
- `replacer(name)` is called at most once per unique marker; the
290
- result is reused for subsequent occurrences.
302
+ `replacer(name)` is called **once per occurrence**, in source
303
+ order. Callers that want back-referencing semantics (e.g.
304
+ `:name` or `$N` sharing one slot across multiple occurrences)
305
+ must cache results themselves.
291
306
  """
292
- mapping = {}
293
- for _, name, _, _ in _FORMATTER.parse(self.text):
294
- if name is not None and name not in mapping:
295
- mapping[name] = replacer(name)
296
- return self.text.format(**mapping)
307
+ parts = []
308
+ for literal, name, _, _ in _FORMATTER.parse(self.text):
309
+ parts.append(literal)
310
+ if name is not None:
311
+ parts.append(replacer(name))
312
+ return ''.join(parts)
297
313
 
298
314
 
299
315
  class Resolver:
@@ -370,19 +386,26 @@ class Resolver:
370
386
  f'unknown binding(s) for this fragment: {sorted(extra)}'
371
387
  )
372
388
 
373
- # Build a per-paramstyle replacer that both collects output
374
- # params/args and returns the placeholder text for each marker.
389
+ # Build a per-paramstyle replacer. Replacers are called once
390
+ # per marker occurrence name-keyed styles cache results so
391
+ # repeated markers share a single param; positional back-ref
392
+ # styles (numeric, dollar-numeric) cache by position; qmark /
393
+ # format emit a fresh arg for every occurrence.
375
394
  missing = []
376
- if paramstyle == 'named':
395
+ if paramstyle in ('named', 'pyformat'):
377
396
  out_params = {}
397
+ fmt = ':{name}' if paramstyle == 'named' else '%({name})s'
378
398
 
379
399
  def replacer(marker):
380
400
  value = cls._lookup_value(sql, marker, bindings, missing)
381
401
  if value is _MISSING:
382
- return '' # errored below
402
+ return ''
383
403
  out_params[marker] = value
384
- return f':{marker}'
385
- else: # dollar-numeric
404
+ return fmt.format(name=marker)
405
+
406
+ out_values = out_params
407
+ elif paramstyle in ('numeric', 'dollar-numeric'):
408
+ prefix = '$' if paramstyle == 'dollar-numeric' else ':'
386
409
  out_args = []
387
410
  pos_by_marker = {}
388
411
 
@@ -393,10 +416,24 @@ class Resolver:
393
416
  if value is _MISSING:
394
417
  return ''
395
418
  out_args.append(value)
396
- ph = f'${len(out_args)}'
419
+ ph = f'{prefix}{len(out_args)}'
397
420
  pos_by_marker[marker] = ph
398
421
  return ph
399
422
 
423
+ out_values = out_args
424
+ else: # qmark, format — no back-reference
425
+ placeholder = '?' if paramstyle == 'qmark' else '%s'
426
+ out_args = []
427
+
428
+ def replacer(marker):
429
+ value = cls._lookup_value(sql, marker, bindings, missing)
430
+ if value is _MISSING:
431
+ return ''
432
+ out_args.append(value)
433
+ return placeholder
434
+
435
+ out_values = out_args
436
+
400
437
  final = sql.substitute(replacer)
401
438
 
402
439
  if missing:
@@ -404,9 +441,7 @@ class Resolver:
404
441
  f'missing binding(s) for substitution(s): '
405
442
  f'{sorted(set(missing))}'
406
443
  )
407
- if paramstyle == 'named':
408
- return final, out_params
409
- return final, out_args
444
+ return final, out_values
410
445
 
411
446
  @staticmethod
412
447
  def _lookup_value(sql, marker, bindings, missing):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.43.7
3
+ Version: 0.43.8
4
4
  Summary: Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms
5
5
  Home-page: https://github.com/freywaid/dotted
6
6
  Author: Frey Waid
@@ -2977,10 +2977,26 @@ Not yet supported:
2977
2977
 
2978
2978
  ### Paramstyles
2979
2979
 
2980
- `Resolver.build(sql, paramstyle='named', **bindings)` defaults to
2981
- PEP 249 named style (`:name`). For Postgres-native `$N` (what asyncpg
2982
- and other raw PG drivers accept), pass `paramstyle='dollar-numeric'`
2983
- the output changes to positional:
2980
+ `Resolver.build(sql, paramstyle='named', **bindings)` renders the
2981
+ fragment in the chosen PEP 249 paramstyle. Supported:
2982
+
2983
+ | paramstyle | placeholder | output shape | typical driver |
2984
+ |-------------------|----------------|--------------|---------------------------|
2985
+ | `named` | `:name` | dict | SQLAlchemy `text`, sqlite3 |
2986
+ | `pyformat` | `%(name)s` | dict | psycopg (v2/v3) named |
2987
+ | `qmark` | `?` | list | sqlite3, ODBC |
2988
+ | `format` | `%s` | list | psycopg positional |
2989
+ | `numeric` | `:1, :2` | list | Oracle-style |
2990
+ | `dollar-numeric` | `$1, $2` | list | asyncpg, native Postgres |
2991
+
2992
+ The dict styles (`named`, `pyformat`) and the numbered positional
2993
+ styles (`numeric`, `dollar-numeric`) have **back-reference**: a
2994
+ substitution used in multiple places in the SQL shares a single bind
2995
+ entry. `qmark` and `format` have no back-reference — each occurrence
2996
+ consumes its own arg, so a repeated substitution emits its value
2997
+ multiple times.
2998
+
2999
+ Examples:
2984
3000
 
2985
3001
  >>> r = dotted.sqlize('(age >= 18 & status = "active")')
2986
3002
  >>> dotted.Resolver.build(r.where, paramstyle='dollar-numeric')
@@ -2990,8 +3006,16 @@ and other raw PG drivers accept), pass `paramstyle='dollar-numeric'`
2990
3006
  >>> dotted.Resolver.build(r.where, paramstyle='dollar-numeric', min_age=30)
2991
3007
  ('age >= $1', [30])
2992
3008
 
2993
- Under `'dollar-numeric'`, the second element is a list of positional
2994
- args ready for `await conn.execute(sql, *args)` asyncpg style.
3009
+ >>> dotted.Resolver.build(r.where, paramstyle='pyformat', min_age=30)
3010
+ ('age >= %(min_age)s', {'min_age': 30})
3011
+
3012
+ Back-reference vs no-backref on repeated substitutions:
3013
+
3014
+ >>> r = dotted.sqlize('(age >= $(x) & weight = $(x))')
3015
+ >>> dotted.Resolver.build(r.where, paramstyle='numeric', x=42)
3016
+ ('(age >= :1) AND (weight = :1)', [42])
3017
+ >>> dotted.Resolver.build(r.where, paramstyle='qmark', x=42)
3018
+ ('(age >= ?) AND (weight = ?)', [42, 42])
2995
3019
 
2996
3020
  A `ParamStyle` `StrEnum` is also available for autocomplete and type
2997
3021
  checking:
@@ -5,7 +5,7 @@ with open("README.md", "rt") as f:
5
5
 
6
6
  setuptools.setup(
7
7
  name="dotted_notation",
8
- version="0.43.7",
8
+ version="0.43.8",
9
9
  author="Frey Waid",
10
10
  author_email="logophage1@gmail.com",
11
11
  description="Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms",
@@ -12,7 +12,14 @@ from dotted.sqlize import (
12
12
 
13
13
  # All supported paramstyles as fixture values. Used to parametrize
14
14
  # scenario tests so each runs under every paramstyle.
15
- PARAMSTYLES = ['named', 'dollar-numeric']
15
+ PARAMSTYLES = ['named', 'pyformat', 'qmark', 'format', 'numeric', 'dollar-numeric']
16
+
17
+ # Styles that share bindings by back-reference (repeated markers collapse
18
+ # to one entry). qmark/format have no back-reference and repeat values.
19
+ BACKREF_STYLES = ['named', 'pyformat', 'numeric', 'dollar-numeric']
20
+
21
+ # Styles that return a dict vs a list.
22
+ DICT_STYLES = ['named', 'pyformat']
16
23
 
17
24
 
18
25
  # ---------------- scalar columns ----------------
@@ -522,7 +529,7 @@ def test_unsupported_flavor_errors():
522
529
  def test_unsupported_paramstyle_errors():
523
530
  r = sqlize('age=30')
524
531
  with pytest.raises(TranslationError, match='unsupported paramstyle'):
525
- Resolver.build(r.where, paramstyle='qmark')
532
+ Resolver.build(r.where, paramstyle='bogus')
526
533
 
527
534
 
528
535
  # ---------------- identifier quoting ----------------
@@ -567,20 +574,32 @@ def test_dollar_numeric_with_subst():
567
574
  def _placeholder(paramstyle, name, position):
568
575
  """
569
576
  Return the expected placeholder string for a given paramstyle.
570
- name: the bind name (for named) — ignored for positional.
577
+ name: the bind name (for name-keyed styles) — ignored for positional.
571
578
  position: 1-based position in the final SQL.
572
579
  """
573
580
  if paramstyle == 'named':
574
581
  return f':{name}'
575
- return f'${position}'
582
+ if paramstyle == 'pyformat':
583
+ return f'%({name})s'
584
+ if paramstyle == 'numeric':
585
+ return f':{position}'
586
+ if paramstyle == 'dollar-numeric':
587
+ return f'${position}'
588
+ if paramstyle == 'qmark':
589
+ return '?'
590
+ if paramstyle == 'format':
591
+ return '%s'
592
+ raise ValueError(paramstyle)
576
593
 
577
594
 
578
595
  def _values(paramstyle, items):
579
596
  """
580
597
  Build the expected params (dict) or args (list) from an ordered
581
- list of (bind_name, value) tuples.
598
+ list of (bind_name, value) tuples. Each test passes items in the
599
+ canonical named-paramstyle shape; this helper converts for
600
+ positional styles.
582
601
  """
583
- if paramstyle == 'named':
602
+ if paramstyle in DICT_STYLES:
584
603
  return dict(items)
585
604
  return [v for _, v in items]
586
605
 
@@ -687,8 +706,9 @@ def test_ps_subst(paramstyle):
687
706
  assert values == _values(paramstyle, [('min_age', 30)])
688
707
 
689
708
 
690
- @pytest.mark.parametrize('paramstyle', PARAMSTYLES)
691
- def test_ps_subst_repeated_shares(paramstyle):
709
+ @pytest.mark.parametrize('paramstyle', BACKREF_STYLES)
710
+ def test_ps_subst_repeated_backref(paramstyle):
711
+ """Styles with back-reference share one slot across occurrences."""
692
712
  r = sqlize('(age >= $(x) & weight = $(x))')
693
713
  sql, values = Resolver.build(r.where, paramstyle=paramstyle, x=42)
694
714
  ph = _placeholder(paramstyle, 'x', 1)
@@ -696,6 +716,16 @@ def test_ps_subst_repeated_shares(paramstyle):
696
716
  assert values == _values(paramstyle, [('x', 42)])
697
717
 
698
718
 
719
+ @pytest.mark.parametrize('paramstyle', ['qmark', 'format'])
720
+ def test_ps_subst_repeated_no_backref(paramstyle):
721
+ """qmark/format repeat the value (no back-reference)."""
722
+ r = sqlize('(age >= $(x) & weight = $(x))')
723
+ sql, values = Resolver.build(r.where, paramstyle=paramstyle, x=42)
724
+ ph = _placeholder(paramstyle, 'x', 1)
725
+ assert sql == f'(age >= {ph}) AND (weight = {ph})'
726
+ assert values == [42, 42]
727
+
728
+
699
729
  @pytest.mark.parametrize('paramstyle', PARAMSTYLES)
700
730
  def test_ps_pattern_with_literal_inline(paramstyle):
701
731
  r = sqlize('data.users[*].age >= 30')
@@ -746,7 +776,7 @@ def test_ps_mixed_pattern_and_scalar(paramstyle):
746
776
 
747
777
  # ---------------- ParamStyle enum accepted ----------------
748
778
 
749
- @pytest.mark.parametrize('style', [ParamStyle.named, ParamStyle.dollar_numeric])
779
+ @pytest.mark.parametrize('style', list(ParamStyle))
750
780
  def test_paramstyle_enum_member_accepted(style):
751
781
  """Enum members work interchangeably with their string values."""
752
782
  r = sqlize('age = 30')