fresco 3.5.0__py3-none-any.whl → 3.7.0__py3-none-any.whl

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.
fresco/routeargs.py CHANGED
@@ -12,6 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  #
15
+ from collections.abc import Sequence
15
16
  from operator import attrgetter, itemgetter, methodcaller
16
17
  from fresco.exceptions import MissingRouteArg
17
18
  from fresco.request import Request
@@ -19,6 +20,7 @@ from itertools import cycle
19
20
  from typing import Any
20
21
  from typing import Callable
21
22
  from typing import Mapping
23
+ from typing import Union
22
24
 
23
25
  __all__ = (
24
26
  "routearg",
@@ -138,7 +140,7 @@ def map_magics(dest):
138
140
 
139
141
 
140
142
  @map_magics("raise_exception")
141
- class LazyException(object):
143
+ class LazyException:
142
144
  """
143
145
  A lazy exception uses magic methods to intercept any access to the object
144
146
  and raise an exception.
@@ -151,7 +153,7 @@ class LazyException(object):
151
153
  raise self.__dict__["_exception"]
152
154
 
153
155
 
154
- class RouteArg(object):
156
+ class RouteArg:
155
157
  """
156
158
  RouteArg objects can be used as keyword arguments in a route definition.
157
159
  RouteArgs can extract information from the request and make it available
@@ -194,7 +196,7 @@ class RouteArg(object):
194
196
  self.route = route
195
197
  self.name = name
196
198
 
197
- def __call__(self, request):
199
+ def __call__(self, request: Request) -> Any:
198
200
  return None
199
201
 
200
202
 
@@ -225,7 +227,7 @@ def routearg(func, *args, **kwargs):
225
227
 
226
228
 
227
229
  class RequestArg(RouteArg):
228
- """\
230
+ """
229
231
  Extract a view keyword argument from the request object.
230
232
  """
231
233
 
@@ -241,9 +243,19 @@ class RequestArg(RouteArg):
241
243
  #: input
242
244
  converter_exceptions = (ValueError, TypeError)
243
245
 
246
+ converter: Union[
247
+ Callable[[list[str]], Any],
248
+ Callable[[str], Any],
249
+ ]
250
+
244
251
  def __init__(
245
252
  self,
246
- converter=None,
253
+ converter: Union[
254
+ Sequence[
255
+ Callable[[str], Any],
256
+ ],
257
+ Callable[[str], Any]
258
+ ] = str,
247
259
  key=None,
248
260
  default=_marker,
249
261
  exception=MissingRouteArg,
@@ -251,11 +263,13 @@ class RequestArg(RouteArg):
251
263
  super(RequestArg, self).__init__(default, exception)
252
264
  self.formkey = key
253
265
  self.getter: Callable[[Mapping], Any]
254
- self.is_list = isinstance(converter, list)
266
+ self.is_list = isinstance(converter, Sequence)
255
267
  if self.is_list:
256
- self.converter = lambda vs: [c(v) for c, v in zip(cycle(converter), vs)]
268
+ def list_converter(vs: list[str]) -> list[Any]:
269
+ return [c(v) for c, v in zip(cycle(converter), vs)] # type: ignore
270
+ self.converter = list_converter
257
271
  else:
258
- self.converter = converter
272
+ self.converter = converter # type: ignore
259
273
 
260
274
  def configure(self, route, name):
261
275
  if self.formkey is None:
@@ -372,7 +386,7 @@ class CookieArg(RequestArg):
372
386
 
373
387
  source = RequestArg.cookies
374
388
 
375
- def getter(self, cookies):
389
+ def getter(self, cookies): # type: ignore
376
390
  if self.is_list:
377
391
  return cookies.getlist(self.formkey)
378
392
  else:
fresco/routing.py CHANGED
@@ -19,18 +19,15 @@ import warnings
19
19
  from copy import copy
20
20
  from collections import defaultdict
21
21
  from collections import namedtuple
22
+ from collections.abc import Collection
22
23
  from collections.abc import MutableSequence
23
24
  from importlib import import_module
24
25
  from functools import partial
25
26
  from typing import Any
26
27
  from typing import Callable
27
- from typing import Dict
28
- from typing import List
29
28
  from typing import Mapping
30
29
  from typing import Optional
31
30
  from typing import Union
32
- from typing import Set
33
- from typing import Tuple
34
31
  from weakref import WeakKeyDictionary
35
32
  import typing as t
36
33
 
@@ -39,7 +36,8 @@ from fresco.response import Response
39
36
  from fresco.request import Request
40
37
  from fresco.requestcontext import context
41
38
  from fresco.routeargs import RouteArg
42
- from fresco.typing import WSGICallable
39
+ from fresco.types import WSGIApplication
40
+ from fresco.types import ViewCallable
43
41
  from fresco.util.cache import make_cache
44
42
  from fresco.util.common import fq_path
45
43
  from fresco.util.urls import join_path
@@ -178,6 +176,7 @@ class RouteTraversal(
178
176
  else:
179
177
  viewspecs = [viewspec]
180
178
  collections_traversed_iter = iter(self.collections_traversed)
179
+ route = None
181
180
  for item in viewspecs:
182
181
  while True:
183
182
  ct = next(collections_traversed_iter, None)
@@ -247,7 +246,7 @@ class Pattern(object):
247
246
  URL path.
248
247
  """
249
248
 
250
- segments: t.List["PatternSegment"]
249
+ segments: list["PatternSegment"]
251
250
 
252
251
  def match(self, path):
253
252
  """
@@ -280,8 +279,8 @@ class Pattern(object):
280
279
  raise NotImplementedError()
281
280
 
282
281
 
283
- class Converter(object):
284
- """\
282
+ class Converter:
283
+ """
285
284
  Responsible for converting arguments to and from URL components.
286
285
 
287
286
  A ``Converter`` class should provide two instance methods:
@@ -339,11 +338,11 @@ class StrConverter(Converter):
339
338
 
340
339
  pattern = r"[^/]+"
341
340
 
342
- def to_string(self, s):
341
+ def to_string(self, ob):
343
342
  """
344
343
  Return ``s`` converted to an ``str`` object.
345
344
  """
346
- return s
345
+ return ob
347
346
 
348
347
  def from_string(self, s):
349
348
  """
@@ -520,7 +519,15 @@ class ExtensiblePattern(Pattern):
520
519
 
521
520
  self.segments = list(self._make_segments())
522
521
  self.args = [item for item in self.segments if item.converter is not None]
523
-
522
+ self.segments_from_string = [
523
+ (s.name, s.converter.from_string)
524
+ for s in self.segments
525
+ if s.converter is not None
526
+ ]
527
+ self.positional_args = tuple(a.converter for a in self.args if a.name is None)
528
+ self.keyword_args = {
529
+ a.name: a.converter for a in self.args if a.name is not None
530
+ }
524
531
  regex = "".join(segment.regex for segment in self.segments)
525
532
  if self.match_entire_path:
526
533
  regex += "$"
@@ -531,9 +538,7 @@ class ExtensiblePattern(Pattern):
531
538
  self.regex_match = self.regex.match
532
539
 
533
540
  def path_argument_info(self):
534
- positional = tuple(a.converter for a in self.args if a.name is None)
535
- keyword = {a.name: a.converter for a in self.args if a.name is not None}
536
- return (positional, keyword)
541
+ return (self.positional_args, self.keyword_args)
537
542
 
538
543
  def _make_segments(self):
539
544
  r"""
@@ -596,8 +601,8 @@ class ExtensiblePattern(Pattern):
596
601
 
597
602
  try:
598
603
  group_items = [
599
- (segment.name, segment.converter.from_string(value))
600
- for value, segment in zip(groups, self.args)
604
+ (name, from_string(value))
605
+ for value, (name, from_string) in zip(groups, self.segments_from_string)
601
606
  ]
602
607
  except ValueError:
603
608
  return None
@@ -607,7 +612,9 @@ class ExtensiblePattern(Pattern):
607
612
  kwargs = {name: value for name, value in group_items if name}
608
613
  return PathMatch(matched, path[len(matched) :], args, kwargs)
609
614
 
610
- def pathfor(self, *args, **kwargs) -> Tuple[str, List[Any], Dict[Any, Any]]:
615
+ def pathfor(
616
+ self, *args, _strjoin="".join, **kwargs
617
+ ) -> tuple[str, list[Any], dict[Any, Any]]:
611
618
  """
612
619
  Example usage::
613
620
 
@@ -621,8 +628,7 @@ class ExtensiblePattern(Pattern):
621
628
  """
622
629
 
623
630
  arg_list = list(args)
624
- kwargs = kwargs
625
- result: List[str] = []
631
+ result: list[str] = []
626
632
  result_append = result.append
627
633
  for seg in self.segments:
628
634
  if not seg.converter:
@@ -631,7 +637,7 @@ class ExtensiblePattern(Pattern):
631
637
  elif seg.name:
632
638
  try:
633
639
  value = kwargs.pop(seg.name)
634
- except IndexError:
640
+ except KeyError:
635
641
  raise URLGenerationError(
636
642
  "Argument %r not specified for url %r"
637
643
  % (seg.name, self.pattern)
@@ -643,11 +649,11 @@ class ExtensiblePattern(Pattern):
643
649
  value = arg_list.pop(0)
644
650
  except IndexError:
645
651
  raise URLGenerationError(
646
- "Not enough positional arguments for url %r" % (self.pattern,)
652
+ f"Not enough positional arguments for url {self.pattern}"
647
653
  )
648
654
  result_append(seg.converter.to_string(value))
649
655
 
650
- return "".join(result), arg_list, kwargs
656
+ return _strjoin(result), arg_list, kwargs
651
657
 
652
658
  def add_prefix(self, prefix):
653
659
  return self.__class__(join_path(prefix, self.pattern), self.match_entire_path)
@@ -680,7 +686,7 @@ class ExtensiblePattern(Pattern):
680
686
  return "%s" % (self.pattern,)
681
687
 
682
688
 
683
- class PatternSegment(object):
689
+ class PatternSegment:
684
690
  """
685
691
  Represent a single segment of a URL pattern, storing information about the
686
692
  ``source``, ``regex`` used to pattern match the segment, ``name`` for
@@ -690,7 +696,13 @@ class PatternSegment(object):
690
696
 
691
697
  __slots__ = ["source", "regex", "name", "converter"]
692
698
 
693
- def __init__(self, source, regex, name, converter):
699
+ def __init__(
700
+ self,
701
+ source: str,
702
+ regex: str,
703
+ name: Optional[str],
704
+ converter: Optional[Converter],
705
+ ):
694
706
  self.source = source
695
707
  self.regex = regex
696
708
  self.name = name
@@ -705,9 +717,9 @@ class Route(object):
705
717
  #: The default class to use for URL pattern matching
706
718
  pattern_class = ExtensiblePattern
707
719
 
708
- fallthrough_statuses: Optional[Set[int]]
720
+ fallthrough_statuses: Optional[set[int]]
709
721
 
710
- _route_hints: Dict[Callable, Dict[str, List[Callable]]] = defaultdict(
722
+ _route_hints: dict[Callable, dict[str, list[Callable]]] = defaultdict(
711
723
  lambda: defaultdict(list)
712
724
  )
713
725
 
@@ -716,9 +728,9 @@ class Route(object):
716
728
  def __init__(
717
729
  self,
718
730
  pattern: t.Union[str, Pattern],
719
- methods=None,
720
- view=None,
721
- kwargs=None,
731
+ methods: Optional[Collection[str]] = None,
732
+ view: Optional[Union[ViewCallable, "RouteCollection", str]] = None,
733
+ kwargs: Optional[dict[str, Any]] = None,
722
734
  args=None,
723
735
  name=None,
724
736
  predicate=None,
@@ -726,7 +738,7 @@ class Route(object):
726
738
  filters=None,
727
739
  fallthrough_on=None,
728
740
  provide_request: Optional[bool] = None,
729
- **_kwargs,
741
+ **_kwargs: ViewCallable,
730
742
  ):
731
743
  """
732
744
  :param pattern: A string that can be compiled into a path pattern
@@ -747,7 +759,7 @@ class Route(object):
747
759
  before invoking it
748
760
  :param filters: Filter functions to apply to the view's return
749
761
  value before returning the final response object
750
- :param fallthrough_on: A List of http status codes which, if returned
762
+ :param fallthrough_on: A list of http status codes which, if returned
751
763
  by a view will cause the current response to be
752
764
  discarded with routing continuing to the next
753
765
  available route.
@@ -802,8 +814,9 @@ class Route(object):
802
814
  'http://localhost/thumbnail'
803
815
 
804
816
  """
805
- method_view_map: Dict[str, Callable] = {}
817
+ method_view_map: dict[str, t.Union[ViewCallable, RouteCollection, str]] = {}
806
818
  if methods:
819
+ assert view is not None
807
820
  if isinstance(methods, str):
808
821
  methods = [methods]
809
822
  else:
@@ -837,7 +850,7 @@ class Route(object):
837
850
  self.name = name
838
851
  self.predicate = predicate
839
852
  self.decorators = decorators or []
840
- self.before_hooks: List[Callable] = []
853
+ self.before_hooks: list[Callable] = []
841
854
  self.filters = filters or []
842
855
  if fallthrough_on:
843
856
  self.fallthrough_statuses = {int(i) for i in fallthrough_on}
@@ -848,14 +861,14 @@ class Route(object):
848
861
  self.provide_request = provide_request
849
862
 
850
863
  #: Default values to use for path generation
851
- self.routed_args_default: Dict[str, Any] = {}
864
+ self.routed_args_default: dict[str, Any] = {}
852
865
 
853
866
  self.pattern = pattern
854
867
  self.methods = set(method_view_map)
855
868
  self.instance = None
856
869
 
857
870
  # Cached references to view functions
858
- self._cached_views: Dict[str, Callable] = {}
871
+ self._cached_views: dict[str, ViewCallable] = {}
859
872
 
860
873
  # Cached references to decorated view function. We use weakrefs in case
861
874
  # a process_view hook substitutes the view function used as a key
@@ -868,8 +881,12 @@ class Route(object):
868
881
  if default is not _marker:
869
882
  self.routed_args_default[k] = default
870
883
 
871
- self.view_args = tuple(args or _kwargs.pop("view_args", tuple()))
872
- self.view_kwargs = dict(kwargs or _kwargs.pop("view_kwargs", {}), **_kwargs)
884
+ self.view_args: tuple[Any] = tuple(
885
+ args or _kwargs.pop("view_args", tuple()) # type: ignore
886
+ )
887
+ self.view_kwargs: dict[str, Any] = dict(
888
+ kwargs or _kwargs.pop("view_kwargs", {}), **_kwargs # type: ignore
889
+ )
873
890
 
874
891
  for arg in self.view_args:
875
892
  if isinstance(arg, RouteArg):
@@ -886,7 +903,9 @@ class Route(object):
886
903
  self.viewspecs = method_view_map
887
904
 
888
905
  def __repr__(self):
889
- view_methods_map: Mapping[Callable, Set[str]] = defaultdict(set)
906
+ view_methods_map: Mapping[
907
+ t.Union[RouteCollection, ViewCallable, str], set[str]
908
+ ] = defaultdict(set)
890
909
  for method, viewspec in self.viewspecs.items():
891
910
  view_methods_map[viewspec].add(method)
892
911
 
@@ -916,7 +935,7 @@ class Route(object):
916
935
  return None
917
936
  return self.pattern.match(path)
918
937
 
919
- def getview(self, method: str) -> Callable[..., Any]:
938
+ def getview(self, method: str) -> ViewCallable:
920
939
  """
921
940
  Resolve and return the raw view callable.
922
941
  """
@@ -937,14 +956,14 @@ class Route(object):
937
956
  raise RouteNotReady()
938
957
  uview = getattr(self.instance, uview)
939
958
 
940
- self._cached_views[method] = uview
959
+ self._cached_views[method] = uview # type: ignore
941
960
  if self.provide_request is None:
942
961
  if callable(uview):
943
962
  self.provide_request = _has_request_parameter(uview)
944
963
  else:
945
964
  self.provide_request = False
946
965
 
947
- return uview
966
+ return uview # type: ignore
948
967
 
949
968
  @classmethod
950
969
  def _add_route_hint(cls, viewfunc, hinttype, func):
@@ -1179,7 +1198,7 @@ class RRoute(Route):
1179
1198
 
1180
1199
  def split_iter(pattern, string):
1181
1200
  """
1182
- Generate alternate strings and match objects for all occurances of
1201
+ Generate alternate strings and match objects for all occurences of
1183
1202
  ``pattern`` in ``string``.
1184
1203
  """
1185
1204
  matcher = pattern.finditer(string)
@@ -1219,10 +1238,8 @@ class RouteCollection(MutableSequence):
1219
1238
  _route_cache = None
1220
1239
 
1221
1240
  def __init__(self, routes=None, route_class=None, cache=True):
1222
- self.__routes__: List[Route] = []
1223
- self.__routed_views__: Dict[
1224
- Union[str, Callable], Union[Route, RouteNotFound]
1225
- ] = {}
1241
+ self.__routes__: list[Route] = []
1242
+ self.__routed_views__: dict[Any, Union[Route, RouteNotFound]] = {}
1226
1243
  if cache:
1227
1244
  self.reinit_route_cache()
1228
1245
  self.route_class = route_class or self.route_class
@@ -1287,8 +1304,8 @@ class RouteCollection(MutableSequence):
1287
1304
  )
1288
1305
  self._route_cache = make_cache(self._get_routes, cache_size)
1289
1306
 
1290
- def insert(self, position, item):
1291
- self.__routes__.insert(position, item)
1307
+ def insert(self, index, value):
1308
+ self.__routes__.insert(index, value)
1292
1309
  if self._route_cache is not None:
1293
1310
  self.reinit_route_cache()
1294
1311
 
@@ -1451,6 +1468,7 @@ class RouteCollection(MutableSequence):
1451
1468
  # View function arguments extracted while traversing the path
1452
1469
  traversal_args, traversal_kwargs = result.args, result.kwargs
1453
1470
  if method is None:
1471
+ view = None
1454
1472
  for m in route.viewspecs:
1455
1473
  view = route.getview(m)
1456
1474
  break
@@ -1555,8 +1573,8 @@ class RouteCollection(MutableSequence):
1555
1573
  def route(
1556
1574
  self,
1557
1575
  pattern: t.Union[str, Pattern],
1558
- methods: t.Optional[t.Union[str, t.Iterable[str]]] = None,
1559
- view: t.Optional[t.Union[str, t.Callable]] = None,
1576
+ methods: t.Optional[t.Union[str, t.Collection[str]]] = None,
1577
+ view: t.Optional[t.Union[str, ViewCallable]] = None,
1560
1578
  *args,
1561
1579
  route_class: t.Optional[t.Type[Route]] = None,
1562
1580
  **kwargs,
@@ -1595,7 +1613,7 @@ class RouteCollection(MutableSequence):
1595
1613
  def route_wsgi(
1596
1614
  self,
1597
1615
  path: str,
1598
- wsgiapp: t.Union[WSGICallable, str],
1616
+ wsgiapp: t.Union[WSGIApplication, str],
1599
1617
  rewrite_script_name: bool = True,
1600
1618
  *args,
1601
1619
  **kwargs,
@@ -1731,7 +1749,7 @@ class RouteCollection(MutableSequence):
1731
1749
  if r is route:
1732
1750
  del self.__routed_views__[k]
1733
1751
 
1734
- def remove(self, viewspec):
1752
+ def remove(self, value):
1735
1753
  """
1736
1754
  Remove the route(s) identified by ``viewspec``
1737
1755
 
@@ -1739,7 +1757,7 @@ class RouteCollection(MutableSequence):
1739
1757
  ('package.module.view_function'), or the name of a
1740
1758
  named route
1741
1759
  """
1742
- self.replace(viewspec, None)
1760
+ self.replace(value, None)
1743
1761
 
1744
1762
 
1745
1763
  class DelegateRoute(Route):
@@ -1762,7 +1780,7 @@ class DelegateRoute(Route):
1762
1780
  view = RouteCollection(routes)
1763
1781
  super(DelegateRoute, self).__init__(pattern, ALL_METHODS, view, *args, **kwargs)
1764
1782
 
1765
- def _dynamic_routecollectionfactory(self, *args, **kwargs):
1783
+ def _dynamic_routecollectionfactory(self, *args, **kwargs) -> RouteCollection:
1766
1784
  """\
1767
1785
  Return the RouteCollection responsible for paths under this route
1768
1786
  """
@@ -1776,8 +1794,8 @@ class DelegateRoute(Route):
1776
1794
  (r.bindto(routes) for r in routes.__routes__), cache=False
1777
1795
  )
1778
1796
 
1779
- def _static_routecollectionfactory(self, *args, **kwargs):
1780
- return self.getview(GET)
1797
+ def _static_routecollectionfactory(self, *args, **kwargs) -> RouteCollection:
1798
+ return self.getview(GET) # type: ignore
1781
1799
 
1782
1800
 
1783
1801
  def register_converter(name, registry=ExtensiblePattern):
fresco/static.py CHANGED
@@ -57,7 +57,7 @@ def serve_static_file(path, content_type=None, bufsize=8192, **kwargs):
57
57
  file_wrapper = request.environ.get("wsgi.file_wrapper")
58
58
  if file_wrapper is not None:
59
59
 
60
- def content_iterator(f):
60
+ def content_iterator(f): # type: ignore
61
61
  return file_wrapper(f, bufsize)
62
62
 
63
63
  else:
fresco/subrequests.py CHANGED
@@ -172,7 +172,7 @@ def subrequest_raw(view, *args, **kwargs) -> Response:
172
172
 
173
173
  response = view(*args, **kwargs)
174
174
 
175
- return response
175
+ return response # type: ignore
176
176
  finally:
177
177
  context.pop()
178
178
 
fresco/tests/test_core.py CHANGED
@@ -178,10 +178,10 @@ class TestFrescoApp(object):
178
178
 
179
179
  def test_get_methods_matches_on_path(self):
180
180
  app = FrescoApp()
181
- app.route("/1", POST, lambda: None)
182
- app.route("/1", PUT, lambda: None)
183
- app.route("/2", GET, lambda: None)
184
- app.route("/2", DELETE, lambda: None)
181
+ app.route("/1", POST, Response)
182
+ app.route("/1", PUT, Response)
183
+ app.route("/2", GET, Response)
184
+ app.route("/2", DELETE, Response)
185
185
 
186
186
  with app.requestcontext() as c:
187
187
  assert app.get_methods(c.request, "/1") == set([POST, PUT])
@@ -88,7 +88,7 @@ class TestMultDict(object):
88
88
 
89
89
  def test_keys(self):
90
90
  m = MultiDict([("a", 1), ("a", 2), ("b", 3)])
91
- i = m.keys()
91
+ i = iter(m.keys())
92
92
  assert next(i) == "a"
93
93
  assert next(i) == "b"
94
94
  with pytest.raises(StopIteration):
@@ -96,7 +96,7 @@ class TestMultDict(object):
96
96
 
97
97
  def test_values(self):
98
98
  m = MultiDict([("a", 1), ("a", 2), ("b", 3)])
99
- i = m.values()
99
+ i = iter(m.values())
100
100
  assert next(i) == 1
101
101
  assert next(i) == 3
102
102
  with pytest.raises(StopIteration):
@@ -106,7 +106,6 @@ class TestOptions(object):
106
106
 
107
107
 
108
108
  class TestOverrideOptions:
109
-
110
109
  def test_override_options_with_object(self):
111
110
  options = Options(foo=1)
112
111
  with override_options(options, {"foo": 2, "bar": "a"}):
@@ -163,7 +162,6 @@ class TestLoadKeyValuePairs:
163
162
 
164
163
 
165
164
  class TestLoadOptions:
166
-
167
165
  def check_loadoptions(self, tmpdir, files, sources="*", tags=[], expected={}):
168
166
  """
169
167
  Write the files indicated in ``sources`` to the given temporary directory,
@@ -212,6 +210,11 @@ class TestLoadOptions:
212
210
  def test_it_loads_py_files(self, tmpdir):
213
211
  self.check_loadoptions(tmpdir, {"a.py": "x = 2 * 2"}, expected={"x": 4})
214
212
 
213
+ def test_py_files_have_options_in_namespace(self, tmpdir):
214
+ self.check_loadoptions(
215
+ tmpdir, {"a.py": "options['foo'] = 'bar'"}, expected={"foo": "bar"}
216
+ )
217
+
215
218
  def test_it_selects_by_tag(self, tmpdir):
216
219
  with self.check_loadoptions(
217
220
  tmpdir,
@@ -237,14 +240,38 @@ class TestLoadOptions:
237
240
  with self.check_loadoptions(
238
241
  tmpdir,
239
242
  {
240
- "a": "a = 0",
241
- "a.dev.txt": "a = ${a}-1",
242
- "a.local.txt": "a = ${a}-2",
243
- "b.dev.txt": "a = ${a}-3",
243
+ "a": "a = 'a'",
244
+ "a.0-dev.txt": "a = ${a} a.dev",
245
+ "a.local.txt": "a = ${a} a.local",
246
+ "b.dev.txt": "a = ${a} b.dev",
247
+ },
248
+ ) as loadopts:
249
+ assert loadopts("*", ["dev", "local"]) == {"a": "a a.dev b.dev a.local"}
250
+ assert loadopts("*", ["local", "dev"]) == {"a": "a a.local a.dev b.dev"}
251
+
252
+ def test_it_loads_in_priority_order(self, tmpdir):
253
+ with self.check_loadoptions(
254
+ tmpdir,
255
+ {
256
+ "a": "a = 'a'",
257
+ "a.100-dev.txt": "a = ${a} a.100-dev",
258
+ "a.local.txt": "a = ${a} a.local",
259
+ "b.dev.txt": "a = ${a} b.dev",
260
+ },
261
+ ) as loadopts:
262
+ assert loadopts("*", ["dev", "local"]) == {"a": "a b.dev a.local a.100-dev"}
263
+ assert loadopts("*", ["local", "dev"]) == {"a": "a a.local b.dev a.100-dev"}
264
+
265
+ def test_it_loads_in_priority_order_without_tags(self, tmpdir):
266
+ with self.check_loadoptions(
267
+ tmpdir,
268
+ {
269
+ "a": "a = a",
270
+ "b.100": "a = ${a} b",
271
+ "a.200.txt": "a = ${a} 100",
244
272
  },
245
273
  ) as loadopts:
246
- assert loadopts("*", ["dev", "local"]) == {"a": "0-1-3-2"}
247
- assert loadopts("*", ["local", "dev"]) == {"a": "0-2-1-3"}
274
+ assert loadopts("*") == {"a": "a b 100"}
248
275
 
249
276
  def test_it_loads_from_os_environ(self, tmpdir):
250
277
  with setenv(a="2"):
@@ -272,7 +299,7 @@ class TestLoadOptions:
272
299
  tmpdir,
273
300
  {"a.txt": "a=1", "b.txt": "b=1"},
274
301
  sources=["a.*", "b.*"],
275
- expected={"a": 1, "b": 1}
302
+ expected={"a": 1, "b": 1},
276
303
  )
277
304
 
278
305
  def test_it_substitutes_from_environment_variables(self, tmpdir):
@@ -281,7 +308,7 @@ class TestLoadOptions:
281
308
  tmpdir,
282
309
  {"a.txt": "a=1", "a.bar.txt": "a=2"},
283
310
  tags=["{FOO}"],
284
- expected={"a": 2}
311
+ expected={"a": 2},
285
312
  )
286
313
 
287
314
  with setenv(FOO="baz"):
@@ -289,7 +316,7 @@ class TestLoadOptions:
289
316
  tmpdir,
290
317
  {"a.txt": "a=1", "a.bar.txt": "a=2"},
291
318
  tags=["{FOO}"],
292
- expected={"a": 1}
319
+ expected={"a": 1},
293
320
  )
294
321
 
295
322
  def test_it_allows_missing_environment_variables(self, tmpdir):
@@ -298,19 +325,16 @@ class TestLoadOptions:
298
325
  tmpdir,
299
326
  {"a.txt": "a=1", "a.bar.txt": "a=2"},
300
327
  tags=["{FOO}"],
301
- expected={"a": 1}
328
+ expected={"a": 1},
302
329
  )
303
330
 
304
331
 
305
332
  class TestDictFromOptions:
306
-
307
333
  def test_it_splits_on_prefix(self):
308
-
309
334
  options = Options(FOO_BAR=1, FOO_BAZ=2, FOO_BAR_BAZ=3, BAR=4)
310
335
  assert dict_from_options("FOO_", options) == {"BAR": 1, "BAZ": 2, "BAR_BAZ": 3}
311
336
 
312
337
  def test_it_splits_recursively(self):
313
-
314
338
  options = Options(
315
339
  A_A=1,
316
340
  A_B_C_D=2,
@@ -323,7 +347,7 @@ class TestDictFromOptions:
323
347
  "A": 1,
324
348
  "B": {"C": {"D": 2}, "E": 3},
325
349
  "F": {"G": {"H": 4}},
326
- "I": 5
350
+ "I": 5,
327
351
  }
328
352
 
329
353