lsst-pex-config 29.2025.4900__tar.gz → 30.0.0__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 (71) hide show
  1. {lsst_pex_config-29.2025.4900/python/lsst_pex_config.egg-info → lsst_pex_config-30.0.0}/PKG-INFO +4 -3
  2. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/doc/lsst.pex.config/CHANGES.rst +23 -0
  3. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/pyproject.toml +3 -2
  4. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/config.py +104 -12
  5. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/configChoiceField.py +1 -1
  6. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/configDictField.py +1 -1
  7. lsst_pex_config-30.0.0/python/lsst/pex/config/version.py +2 -0
  8. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0/python/lsst_pex_config.egg-info}/PKG-INFO +4 -3
  9. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst_pex_config.egg-info/requires.txt +1 -0
  10. lsst_pex_config-30.0.0/tests/test__file__.py +103 -0
  11. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/tests/test_configChoiceField.py +14 -0
  12. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/tests/test_configDictField.py +13 -0
  13. lsst_pex_config-29.2025.4900/python/lsst/pex/config/version.py +0 -2
  14. lsst_pex_config-29.2025.4900/tests/test__file__.py +0 -55
  15. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/COPYRIGHT +0 -0
  16. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/LICENSE +0 -0
  17. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/MANIFEST.in +0 -0
  18. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/README.rst +0 -0
  19. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/bsd_license.txt +0 -0
  20. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/doc/lsst.pex.config/design-notes.rst +0 -0
  21. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/doc/lsst.pex.config/field-types.rst +0 -0
  22. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/doc/lsst.pex.config/index.rst +0 -0
  23. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/doc/lsst.pex.config/inspecting-configs.rst +0 -0
  24. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/doc/lsst.pex.config/overview.rst +0 -0
  25. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/doc/lsst.pex.config/registry-intro.rst +0 -0
  26. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/doc/lsst.pex.config/wrapping-cpp-control-objects.rst +0 -0
  27. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/gpl-v3.0.txt +0 -0
  28. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/__init__.py +0 -0
  29. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/__init__.py +0 -0
  30. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/__init__.py +0 -0
  31. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/_doNotImportMe.py +0 -0
  32. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/callStack.py +0 -0
  33. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/choiceField.py +0 -0
  34. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/comparison.py +0 -0
  35. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/configField.py +0 -0
  36. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/configurableActions/__init__.py +0 -0
  37. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/configurableActions/_configurableAction.py +0 -0
  38. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/configurableActions/_configurableActionField.py +0 -0
  39. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/configurableActions/_configurableActionStructField.py +0 -0
  40. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/configurableActions/tests.py +0 -0
  41. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/configurableField.py +0 -0
  42. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/convert.py +0 -0
  43. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/dictField.py +0 -0
  44. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/history.py +0 -0
  45. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/listField.py +0 -0
  46. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/py.typed +0 -0
  47. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/rangeField.py +0 -0
  48. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/registry.py +0 -0
  49. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst/pex/config/wrap.py +0 -0
  50. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst_pex_config.egg-info/SOURCES.txt +0 -0
  51. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst_pex_config.egg-info/dependency_links.txt +0 -0
  52. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst_pex_config.egg-info/top_level.txt +0 -0
  53. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/python/lsst_pex_config.egg-info/zip-safe +0 -0
  54. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/setup.cfg +0 -0
  55. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/tests/testLib.py +0 -0
  56. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/tests/test_Config.py +0 -0
  57. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/tests/test_configurableActions.py +0 -0
  58. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/tests/test_configurableField.py +0 -0
  59. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/tests/test_dictField.py +0 -0
  60. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/tests/test_history.py +0 -0
  61. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/tests/test_listField.py +0 -0
  62. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/tests/test_registry.py +0 -0
  63. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/tests/test_ticket1911.py +0 -0
  64. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/tests/test_ticket1914.py +0 -0
  65. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/tests/test_ticket1915.py +0 -0
  66. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/tests/test_ticket1929.py +0 -0
  67. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/tests/test_ticket1995.py +0 -0
  68. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/tests/test_ticket2818.py +0 -0
  69. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/tests/test_ticketDM-7337.py +0 -0
  70. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/tests/test_unloaded_yaml.py +0 -0
  71. {lsst_pex_config-29.2025.4900 → lsst_pex_config-30.0.0}/tests/test_wrap.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-pex-config
3
- Version: 29.2025.4900
3
+ Version: 30.0.0
4
4
  Summary: A flexible configuration system using Python files.
5
5
  Author-email: Rubin Observatory Data Management <dm-admin@lists.lsst.org>
6
6
  License-Expression: BSD-3-Clause OR GPL-3.0-or-later
@@ -9,12 +9,12 @@ Keywords: lsst
9
9
  Classifier: Intended Audience :: Science/Research
10
10
  Classifier: Operating System :: OS Independent
11
11
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.14
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Topic :: Scientific/Engineering :: Astronomy
17
- Requires-Python: >=3.10.0
17
+ Requires-Python: >=3.11.0
18
18
  Description-Content-Type: text/x-rst
19
19
  License-File: COPYRIGHT
20
20
  License-File: LICENSE
@@ -22,6 +22,7 @@ License-File: gpl-v3.0.txt
22
22
  License-File: bsd_license.txt
23
23
  Requires-Dist: pyyaml>=5.1
24
24
  Requires-Dist: numpy>=1.17
25
+ Requires-Dist: lsst-resources
25
26
  Provides-Extra: test
26
27
  Requires-Dist: pytest>=3.2; extra == "test"
27
28
  Requires-Dist: pytest-openfiles>=0.5.0; extra == "test"
@@ -1,3 +1,26 @@
1
+ lsst-pex-config v30.0.0 (2026-01-15)
2
+ ====================================
3
+
4
+ Python 3.11 is now the minimum supported version.
5
+ Tested on Python 3.14.
6
+
7
+ New Features
8
+ ------------
9
+
10
+ - Added a ``copy`` method that unfreezes. (`DM-16523 <https://rubinobs.atlassian.net/browse/DM-16523>`_)
11
+ - * Changed the config loader method such that if a relative path is specified inside another config it is treated as being relative to that original config file.
12
+ * Modified the config loader to support URI schemes ``file``, ``resource``, and ``eups`` supported by ``lsst-resources``.
13
+ This change means that in some cases ``__file__`` can be a URI string and not a simple path. (`DM-33226 <https://rubinobs.atlassian.net/browse/DM-33226>`_)
14
+
15
+
16
+ Bug Fixes
17
+ ---------
18
+
19
+ - Fixed a bug that caused history-printing to fail when the initial value for a field was `None`. (`DM-51850 <https://rubinobs.atlassian.net/browse/DM-51850>`_)
20
+ - Fixed copying of ``ConfigDictField``. (`DM-53767 <https://rubinobs.atlassian.net/browse/DM-53767>`_)
21
+ - Fixed copying of ``ConfigChoiceField`` and ``RegistryField``. (`DM-53791 <https://rubinobs.atlassian.net/browse/DM-53791>`_)
22
+
23
+
1
24
  lsst-pex-config v29.0.0 (2025-03-25)
2
25
  ====================================
3
26
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lsst-pex-config"
7
- requires-python = ">=3.10.0"
7
+ requires-python = ">=3.11.0"
8
8
  description = "A flexible configuration system using Python files."
9
9
  license = "BSD-3-Clause OR GPL-3.0-or-later"
10
10
  license-files = ["COPYRIGHT", "LICENSE", "gpl-v3.0.txt", "bsd_license.txt"]
@@ -16,7 +16,7 @@ classifiers = [
16
16
  "Intended Audience :: Science/Research",
17
17
  "Operating System :: OS Independent",
18
18
  "Programming Language :: Python :: 3",
19
- "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.14",
20
20
  "Programming Language :: Python :: 3.11",
21
21
  "Programming Language :: Python :: 3.12",
22
22
  "Programming Language :: Python :: 3.13",
@@ -26,6 +26,7 @@ keywords = ["lsst"]
26
26
  dependencies = [
27
27
  "pyyaml >=5.1",
28
28
  "numpy >= 1.17",
29
+ "lsst-resources",
29
30
  ]
30
31
  dynamic = ["version"]
31
32
 
@@ -38,6 +38,7 @@ __all__ = (
38
38
  import copy
39
39
  import importlib
40
40
  import io
41
+ import logging
41
42
  import math
42
43
  import numbers
43
44
  import os
@@ -47,9 +48,13 @@ import sys
47
48
  import tempfile
48
49
  import warnings
49
50
  from collections.abc import Mapping
51
+ from contextlib import contextmanager
52
+ from contextvars import ContextVar
50
53
  from types import GenericAlias
51
54
  from typing import Any, ForwardRef, Generic, TypeVar, cast, overload
52
55
 
56
+ from lsst.resources import ResourcePath, ResourcePathExpression
57
+
53
58
  # if YAML is not available that's fine and we simply don't register
54
59
  # the yaml representer since we know it won't be used.
55
60
  try:
@@ -74,6 +79,25 @@ else:
74
79
  YamlLoaders = ()
75
80
  doImport = None
76
81
 
82
+ _LOG = logging.getLogger(__name__)
83
+
84
+
85
+ # Tracks the current config directory for the current context.
86
+ _config_dir_stack: ContextVar[ResourcePath | None] = ContextVar("_config_dir_stack", default=None)
87
+
88
+
89
+ def _get_config_root() -> ResourcePath | None:
90
+ return _config_dir_stack.get()
91
+
92
+
93
+ @contextmanager
94
+ def _push_config_root(dirname: ResourcePath):
95
+ token = _config_dir_stack.set(dirname)
96
+ try:
97
+ yield
98
+ finally:
99
+ _config_dir_stack.reset(token)
100
+
77
101
 
78
102
  class _PexConfigGenericAlias(GenericAlias):
79
103
  """A Subclass of python's GenericAlias used in defining and instantiating
@@ -1171,15 +1195,51 @@ class Config(metaclass=ConfigMeta): # type: ignore
1171
1195
  e.add_note(f"No field of name {name} exists in config type {_typeStr(self)}")
1172
1196
  raise
1173
1197
 
1198
+ def _filename_to_resource(
1199
+ self, filename: ResourcePathExpression | None = None
1200
+ ) -> tuple[ResourcePath | None, str]:
1201
+ """Create resource path from filename.
1202
+
1203
+ Parameters
1204
+ ----------
1205
+ filename : `lsst.resources.ResourcePathExpression` or `None`
1206
+ The URI expression associated with this config. Can be `None`
1207
+ if no file URI is known.
1208
+
1209
+ Returns
1210
+ -------
1211
+ resource : `lsst.resources.ResourcePath` or `None`
1212
+ The resource version of the filename. Returns `None` if no filename
1213
+ was given or refers to unspecified value.
1214
+ file_string : `str`
1215
+ String form of the resource for use in ``__file__``
1216
+ """
1217
+ if filename is None or filename in ("?", "<unknown>"):
1218
+ return None, "<unknown>"
1219
+ base = _get_config_root()
1220
+ resource = ResourcePath(filename, forceAbsolute=True, forceDirectory=False, root=base)
1221
+
1222
+ # Preferred definition of __file__ is the full OS path. If a config
1223
+ # is loaded with a relative path it must be converted to the absolute
1224
+ # path to avoid confusion with later relative paths referenced inside
1225
+ # the config.
1226
+ if resource.scheme == "file":
1227
+ file_string = resource.ospath
1228
+ else:
1229
+ file_string = str(resource)
1230
+
1231
+ return resource, file_string
1232
+
1174
1233
  def load(self, filename, root="config"):
1175
1234
  """Modify this config in place by executing the Python code in a
1176
1235
  configuration file.
1177
1236
 
1178
1237
  Parameters
1179
1238
  ----------
1180
- filename : `str`
1181
- Name of the configuration file. A configuration file is Python
1182
- module.
1239
+ filename : `lsst.resources.ResourcePathExpression`
1240
+ Name of the configuration URI. A configuration file is a Python
1241
+ module. Since configuration files are Python code, remote URIs
1242
+ are not allowed.
1183
1243
  root : `str`, optional
1184
1244
  Name of the variable in file that refers to the config being
1185
1245
  overridden.
@@ -1199,9 +1259,21 @@ class Config(metaclass=ConfigMeta): # type: ignore
1199
1259
  lsst.pex.config.Config.saveToStream
1200
1260
  lsst.pex.config.Config.saveToString
1201
1261
  """
1202
- with open(filename) as f:
1203
- code = compile(f.read(), filename=filename, mode="exec")
1204
- self.loadFromString(code, root=root, filename=filename)
1262
+ resource, file_string = self._filename_to_resource(filename)
1263
+ if resource is None:
1264
+ # A filename is required.
1265
+ raise ValueError(f"Undefined URI provided to load command: {filename}.")
1266
+
1267
+ if resource.scheme not in ("file", "eups", "resource"):
1268
+ raise ValueError(f"Remote URI ({resource}) can not be used to load configurations.")
1269
+
1270
+ # Push the directory of the file we are now reading onto the stack
1271
+ # so that nested loads are relative to this file.
1272
+ with _push_config_root(resource.dirname()):
1273
+ _LOG.debug("Updating config from URI %s", str(resource))
1274
+ with resource.open("r") as f:
1275
+ code = compile(f.read(), filename=file_string, mode="exec")
1276
+ self._loadFromString(code, root=root, filename=file_string)
1205
1277
 
1206
1278
  def loadFromStream(self, stream, root="config", filename=None, extraLocals=None):
1207
1279
  """Modify this Config in place by executing the Python code in the
@@ -1224,7 +1296,8 @@ class Config(metaclass=ConfigMeta): # type: ignore
1224
1296
  Then this config's field ``myField`` is set to ``5``.
1225
1297
  filename : `str`, optional
1226
1298
  Name of the configuration file, or `None` if unknown or contained
1227
- in the stream. Used for error reporting.
1299
+ in the stream. Used for error reporting and to set ``__file__``
1300
+ variable in config.
1228
1301
  extraLocals : `dict` of `str` to `object`, optional
1229
1302
  Any extra variables to include in local scope when loading.
1230
1303
 
@@ -1244,7 +1317,7 @@ class Config(metaclass=ConfigMeta): # type: ignore
1244
1317
  """
1245
1318
  if hasattr(stream, "read"):
1246
1319
  if filename is None:
1247
- filename = getattr(stream, "name", "?")
1320
+ filename = getattr(stream, "name", "<unknown>")
1248
1321
  code = compile(stream.read(), filename=filename, mode="exec")
1249
1322
  else:
1250
1323
  code = stream
@@ -1268,9 +1341,11 @@ class Config(metaclass=ConfigMeta): # type: ignore
1268
1341
  config.myField = 5
1269
1342
 
1270
1343
  Then this config's field ``myField`` is set to ``5``.
1271
- filename : `str`, optional
1272
- Name of the configuration file, or `None` if unknown or contained
1273
- in the stream. Used for error reporting.
1344
+ filename : `lsst.resources.ResourcePathExpression`, optional
1345
+ URI of the configuration file, or `None` if unknown or contained
1346
+ in the stream. Used for error reporting and to set ``__file__``
1347
+ variable. Required to be set if the string config attempts to
1348
+ load other configs using either relative path or ``__file__``.
1274
1349
  extraLocals : `dict` of `str` to `object`, optional
1275
1350
  Any extra variables to include in local scope when loading.
1276
1351
 
@@ -1291,7 +1366,24 @@ class Config(metaclass=ConfigMeta): # type: ignore
1291
1366
  if filename is None:
1292
1367
  # try to determine the file name; a compiled string
1293
1368
  # has attribute "co_filename",
1294
- filename = getattr(code, "co_filename", "?")
1369
+ filename = getattr(code, "co_filename", "<unknown>")
1370
+
1371
+ resource, file_string = self._filename_to_resource(filename)
1372
+ if resource is None:
1373
+ # No idea where this config came from so no ability to deal with
1374
+ # relative paths. No reason to use context.
1375
+ self._loadFromString(code, root=root, filename=filename, extraLocals=extraLocals)
1376
+ else:
1377
+ # Push the directory of the file we are now reading onto the stack
1378
+ # so that nested loads are relative to this file.
1379
+ with _push_config_root(resource.dirname()):
1380
+ self._loadFromString(code, root=root, filename=file_string, extraLocals=extraLocals)
1381
+
1382
+ def _loadFromString(self, code, root="config", filename=None, extraLocals=None):
1383
+ """Update config from string.
1384
+
1385
+ Assumes relative directory path context has been setup by caller.
1386
+ """
1295
1387
  with RecordingImporter() as importer:
1296
1388
  globals = {"__file__": filename}
1297
1389
  local = {root: self}
@@ -192,7 +192,7 @@ class ConfigInstanceDict(collections.abc.Mapping[str, Config]):
192
192
  result._typemap = self._typemap
193
193
  if self._selection is not None:
194
194
  if self._field.multi:
195
- result._selection = SelectionSet(result._dict, self._selection._set)
195
+ result._selection = SelectionSet(self, self._selection._set)
196
196
  else:
197
197
  result._selection = self._selection
198
198
  return result
@@ -76,7 +76,7 @@ class ConfigDict(Dict[str, Config]):
76
76
  return type(self)(
77
77
  config,
78
78
  self._field,
79
- {k: v._copy() for k, v in self._dict.items()},
79
+ {k: v.copy() for k, v in self._dict.items()},
80
80
  at=None,
81
81
  label="copy",
82
82
  setHistory=False,
@@ -0,0 +1,2 @@
1
+ __all__ = ["__version__"]
2
+ __version__ = "30.0.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-pex-config
3
- Version: 29.2025.4900
3
+ Version: 30.0.0
4
4
  Summary: A flexible configuration system using Python files.
5
5
  Author-email: Rubin Observatory Data Management <dm-admin@lists.lsst.org>
6
6
  License-Expression: BSD-3-Clause OR GPL-3.0-or-later
@@ -9,12 +9,12 @@ Keywords: lsst
9
9
  Classifier: Intended Audience :: Science/Research
10
10
  Classifier: Operating System :: OS Independent
11
11
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.14
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Topic :: Scientific/Engineering :: Astronomy
17
- Requires-Python: >=3.10.0
17
+ Requires-Python: >=3.11.0
18
18
  Description-Content-Type: text/x-rst
19
19
  License-File: COPYRIGHT
20
20
  License-File: LICENSE
@@ -22,6 +22,7 @@ License-File: gpl-v3.0.txt
22
22
  License-File: bsd_license.txt
23
23
  Requires-Dist: pyyaml>=5.1
24
24
  Requires-Dist: numpy>=1.17
25
+ Requires-Dist: lsst-resources
25
26
  Provides-Extra: test
26
27
  Requires-Dist: pytest>=3.2; extra == "test"
27
28
  Requires-Dist: pytest-openfiles>=0.5.0; extra == "test"
@@ -1,5 +1,6 @@
1
1
  pyyaml>=5.1
2
2
  numpy>=1.17
3
+ lsst-resources
3
4
 
4
5
  [test]
5
6
  pytest>=3.2
@@ -0,0 +1,103 @@
1
+ # This file is part of pex_config.
2
+ #
3
+ # Developed for the LSST Data Management System.
4
+ # This product includes software developed by the LSST Project
5
+ # (http://www.lsst.org).
6
+ # See the COPYRIGHT file at the top-level directory of this distribution
7
+ # for details of code ownership.
8
+ #
9
+ # This software is dual licensed under the GNU General Public License and also
10
+ # under a 3-clause BSD license. Recipients may choose which of these licenses
11
+ # to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12
+ # respectively. If you choose the GPL option then the following text applies
13
+ # (but note that there is still no warranty even if you opt for BSD instead):
14
+ #
15
+ # This program is free software: you can redistribute it and/or modify
16
+ # it under the terms of the GNU General Public License as published by
17
+ # the Free Software Foundation, either version 3 of the License, or
18
+ # (at your option) any later version.
19
+ #
20
+ # This program is distributed in the hope that it will be useful,
21
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
22
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
+ # GNU General Public License for more details.
24
+ #
25
+ # You should have received a copy of the GNU General Public License
26
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
+
28
+ import os
29
+ import unittest
30
+
31
+ import lsst.pex.config as pexConf
32
+ from lsst.resources import ResourcePath
33
+
34
+ TESTDIR = os.path.dirname(os.path.abspath(__file__))
35
+
36
+
37
+ class FileConfig(pexConf.Config):
38
+ """Config used for testing __file__."""
39
+
40
+ number = pexConf.Field("FileConfig.number", int, default=0)
41
+ filename = pexConf.Field("FileConfig.filename", str, default=None)
42
+
43
+
44
+ class FilenameTestCase(unittest.TestCase):
45
+ """Check that __file__ can be used in a config file."""
46
+
47
+ def test__file(self):
48
+ fileUri = ResourcePath(f"{TESTDIR}/config/filename.py", forceAbsolute=True, forceDirectory=False)
49
+ for confFile in (os.path.join(TESTDIR, "config", "filename.py"), fileUri, str(fileUri)):
50
+ c = FileConfig()
51
+ c.load(confFile)
52
+ # The __file__ is always the ospath form.
53
+ self.assertEqual(c.filename, os.path.join(TESTDIR, "config", "filename.py"))
54
+ self.assertEqual(c.number, 5)
55
+
56
+ c = FileConfig()
57
+ with fileUri.open("r") as fh:
58
+ c.loadFromStream(fh)
59
+ self.assertEqual(c.filename, os.path.join(TESTDIR, "config", "filename.py"))
60
+ self.assertEqual(c.number, 5)
61
+
62
+ c = FileConfig()
63
+ data = fileUri.read()
64
+ c.loadFromString(data)
65
+ self.assertEqual(c.filename, "<unknown>")
66
+
67
+ c.loadFromString(data, filename=fileUri.ospath)
68
+ self.assertEqual(c.filename, fileUri.ospath)
69
+
70
+ c = FileConfig()
71
+ with self.assertRaises(ValueError):
72
+ # Use mem scheme because we do not support it for config
73
+ # loading and it does not require additional dependencies
74
+ # such as requests or boto3 to be available.
75
+ c.load("mem://not_there.py")
76
+
77
+ def test_relative(self):
78
+ fileUri = ResourcePath(f"{TESTDIR}/config/relfilename.py", forceAbsolute=True, forceDirectory=False)
79
+ for confFile in (os.path.join(TESTDIR, "config", "relfilename.py"), fileUri, str(fileUri)):
80
+ c = FileConfig()
81
+ c.load(confFile)
82
+ # The __file__ is always the ospath form and should be the file
83
+ # loaded by the config.
84
+ self.assertEqual(c.filename, os.path.join(TESTDIR, "config", "filename.py"))
85
+ self.assertEqual(c.number, 5)
86
+
87
+ c = FileConfig()
88
+ with fileUri.open("r") as fh:
89
+ c.loadFromStream(fh)
90
+ self.assertEqual(c.filename, os.path.join(TESTDIR, "config", "filename.py"))
91
+ self.assertEqual(c.number, 5)
92
+
93
+ c = FileConfig()
94
+ data = fileUri.read()
95
+ with self.assertRaises(FileNotFoundError):
96
+ c.loadFromString(data)
97
+
98
+ c.loadFromString(data, filename=fileUri.ospath)
99
+ self.assertEqual(c.filename, os.path.join(TESTDIR, "config", "filename.py"))
100
+
101
+
102
+ if __name__ == "__main__":
103
+ unittest.main()
@@ -165,6 +165,20 @@ class ConfigChoiceFieldTest(unittest.TestCase):
165
165
  with self.assertRaises(pexConfig.UnexpectedProxyUsageError):
166
166
  pickle.dumps(self.config.c.names)
167
167
 
168
+ def test_copy(self):
169
+ """Test the copy method on a ConfigChoiceField."""
170
+ copy1: Config3 = self.config.copy()
171
+ copy1.a["AAA"].f = 1
172
+ copy1.a["BBB"].f = 1.0
173
+ copy1.a = "BBB"
174
+ self.assertEqual(self.config.a.name, "AAA")
175
+ self.assertEqual(self.config.a.active.f, 4)
176
+ self.assertEqual(self.config.a["AAA"].f, 4)
177
+ self.assertEqual(self.config.a["BBB"].f, 0.5)
178
+ self.assertEqual(copy1.a.name, "BBB")
179
+ self.assertEqual(copy1.a["AAA"].f, 1)
180
+ self.assertEqual(copy1.a["BBB"].f, 1.0)
181
+
168
182
 
169
183
  if __name__ == "__main__":
170
184
  unittest.main()
@@ -207,6 +207,19 @@ class ConfigDictFieldTest(unittest.TestCase):
207
207
 
208
208
  self.assertTrue(pexConfig.compareConfigs("test", c1, c2))
209
209
 
210
+ def test_copy(self):
211
+ """Test that the copy method works on ConfigDictField instances."""
212
+ original = Config2()
213
+ original.d1 = {"a": Config1, "b": Config1(f=4.0)}
214
+ original.freeze()
215
+ copy1 = original.copy()
216
+ self.assertEqual(copy1.d1["a"].f, 3.0)
217
+ self.assertEqual(copy1.d1["b"].f, 4.0)
218
+ copy1.d1["a"].f = 6.0
219
+ self.assertEqual(copy1.d1["a"].f, 6.0)
220
+ self.assertEqual(copy1.d1["b"].f, 4.0)
221
+ self.assertEqual(original.d1["a"].f, 3.0)
222
+
210
223
 
211
224
  if __name__ == "__main__":
212
225
  unittest.main()
@@ -1,2 +0,0 @@
1
- __all__ = ["__version__"]
2
- __version__ = "29.2025.4900"
@@ -1,55 +0,0 @@
1
- # This file is part of pex_config.
2
- #
3
- # Developed for the LSST Data Management System.
4
- # This product includes software developed by the LSST Project
5
- # (http://www.lsst.org).
6
- # See the COPYRIGHT file at the top-level directory of this distribution
7
- # for details of code ownership.
8
- #
9
- # This software is dual licensed under the GNU General Public License and also
10
- # under a 3-clause BSD license. Recipients may choose which of these licenses
11
- # to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12
- # respectively. If you choose the GPL option then the following text applies
13
- # (but note that there is still no warranty even if you opt for BSD instead):
14
- #
15
- # This program is free software: you can redistribute it and/or modify
16
- # it under the terms of the GNU General Public License as published by
17
- # the Free Software Foundation, either version 3 of the License, or
18
- # (at your option) any later version.
19
- #
20
- # This program is distributed in the hope that it will be useful,
21
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
22
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
- # GNU General Public License for more details.
24
- #
25
- # You should have received a copy of the GNU General Public License
26
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
-
28
- import os
29
- import unittest
30
-
31
- import lsst.pex.config as pexConf
32
-
33
- TESTDIR = os.path.dirname(os.path.abspath(__file__))
34
-
35
-
36
- class FileConfig(pexConf.Config):
37
- """Config used for testing __file__."""
38
-
39
- number = pexConf.Field("FileConfig.number", int, default=0)
40
- filename = pexConf.Field("FileConfig.filename", str, default=None)
41
-
42
-
43
- class FilenameTestCase(unittest.TestCase):
44
- """Check that __file__ can be used in a config file."""
45
-
46
- def test__file(self):
47
- c = FileConfig()
48
- confFile = os.path.join(TESTDIR, "config", "filename.py")
49
- c.load(confFile)
50
- self.assertEqual(c.filename, confFile)
51
- self.assertEqual(c.number, 5)
52
-
53
-
54
- if __name__ == "__main__":
55
- unittest.main()