pytrilogy 0.0.3.4__py3-none-any.whl → 0.0.3.6__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.

Potentially problematic release.


This version of pytrilogy might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pytrilogy
3
- Version: 0.0.3.4
3
+ Version: 0.0.3.6
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,8 +1,8 @@
1
- trilogy/__init__.py,sha256=GsU58IPbyhWVC4PT599Dyal9U1Q1BIKeIhpRY0_115Y,302
1
+ trilogy/__init__.py,sha256=R9yJSDmZQvpxjWMcp4mGFnbg7xuxUCiIVrvP8eacKj4,302
2
2
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  trilogy/constants.py,sha256=qZ1d0hoKPPV2HHCoFwPYTVB7b6bXjpWvXd3lE-zEhy8,1494
4
4
  trilogy/engine.py,sha256=yOPnR7XCjWG82Gym_LLZBkYKKJdLCvqdCyt8zguNcnM,1103
5
- trilogy/executor.py,sha256=nvi8F8ls7stAXvYUIRs6zh8X4q6O_plZcfazPnL-hKw,16745
5
+ trilogy/executor.py,sha256=sssEPDnIDPiQtMSrt5pFiJXUfcDc6gSi4m2Eliod_BM,16844
6
6
  trilogy/parser.py,sha256=o4cfk3j3yhUFoiDKq9ZX_GjBF3dKhDjXEwb63rcBkBM,293
7
7
  trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  trilogy/utility.py,sha256=euQccZLKoYBz0LNg5tzLlvv2YHvXh9HArnYp1V3uXsM,763
@@ -18,14 +18,14 @@ trilogy/core/functions.py,sha256=7Pq9jYSJd45L2pxT7AI-_rXVZmeLnmTPp8d1lA4z4Vk,244
18
18
  trilogy/core/graph_models.py,sha256=z17EoO8oky2QOuO6E2aMWoVNKEVJFhLdsQZOhC4fNLU,2079
19
19
  trilogy/core/internal.py,sha256=iicDBlC6nM8d7e7jqzf_ZOmpUsW8yrr2AA8AqEiLx-s,1577
20
20
  trilogy/core/optimization.py,sha256=xGO8piVsLrpqrx-Aid_Y56_5slSv4eZmlP64hCHRiEc,7957
21
- trilogy/core/query_processor.py,sha256=HyDxBhQsD9KX-Y7pYznlpCAW6AvI76RqPTQNa1mreoE,19450
21
+ trilogy/core/query_processor.py,sha256=Do8YpdPBdsbKtl9n37hobzk8SORMGqH-e_zNNxd-BE4,19456
22
22
  trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
- trilogy/core/models/author.py,sha256=ydg1FwMjL58AWsM5cJTB0B8AR8jFaxAZdXfQroiTC0M,67264
23
+ trilogy/core/models/author.py,sha256=oRCKWhz-i1fO1LlHWiHE3l1awCHdQ3yx6FKH9n9RxRU,67188
24
24
  trilogy/core/models/build.py,sha256=kiq31T8LtUtgmT37m617Q2MlMvQTuAxJzwb6947EiWU,56127
25
25
  trilogy/core/models/build_environment.py,sha256=8UggvlPU708GZWYPJMc_ou2r7M3TY2g69eqGvz03YX0,5528
26
26
  trilogy/core/models/core.py,sha256=yie1uuq62uOQ5fjob9NMJbdvQPrCErXUT7JTCuYRyjI,9697
27
27
  trilogy/core/models/datasource.py,sha256=c0tGxyH2WwTmAD047tr69U0a6GNVf-ug26H68yii7DA,9257
28
- trilogy/core/models/environment.py,sha256=QSl-H6nwarzKbQgNRjtwDKMJtA4F_GVQpRs-NMNt-6Q,24983
28
+ trilogy/core/models/environment.py,sha256=h06y1Dv7naw2GuFFAAyoFZmicG7a7Lu-dRoYPVfrOGo,25967
29
29
  trilogy/core/models/execute.py,sha256=ABylFQgtavjjCfFkEsFdUwfMB4UBQLHjdzQ9E67QlAE,33521
30
30
  trilogy/core/optimizations/__init__.py,sha256=EBanqTXEzf1ZEYjAneIWoIcxtMDite5-n2dQ5xcfUtg,356
31
31
  trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnbnb6vdVIztUfFQZsGnM,513
@@ -33,7 +33,7 @@ trilogy/core/optimizations/inline_constant.py,sha256=lvNTIXaLNkw3HseJyXyDNk5R52d
33
33
  trilogy/core/optimizations/inline_datasource.py,sha256=AHuTGh2x0GQ8usOe0NiFncfTFQ_KogdgDl4uucmhIbI,4241
34
34
  trilogy/core/optimizations/predicate_pushdown.py,sha256=g4AYE8Aw_iMlAh68TjNXGP754NTurrDduFECkUjoBnc,9399
35
35
  trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
- trilogy/core/processing/concept_strategies_v3.py,sha256=0rFnasSQlkXTRIfAFMHyHVux1pTQ3ryKeQds-SFSot0,40290
36
+ trilogy/core/processing/concept_strategies_v3.py,sha256=wPlpg4L7uw-f0DgJBkI8VRdcisjDT1X6iApjEE6CmfA,40291
37
37
  trilogy/core/processing/graph_utils.py,sha256=8QUVrkE9j-9C1AyrCb1nQEh8daCe0u1HuXl-Te85lag,1205
38
38
  trilogy/core/processing/utility.py,sha256=Oc5tLGeDDpzhbfo2ZcF8ex1kez-NcJDMcG2Lm5BjS4c,20548
39
39
  trilogy/core/processing/node_generators/__init__.py,sha256=o8rOFHPSo-s_59hREwXMW6gjUJCsiXumdbJNozHUf-Y,800
@@ -88,14 +88,14 @@ trilogy/parsing/common.py,sha256=yAE3x4SyO4PfAb7HhZ_l9sNPYaf_pcM1K8ioEy76SCU,203
88
88
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
89
89
  trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
90
90
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
91
- trilogy/parsing/parse_engine.py,sha256=5SZ572CMcmSFv-LIIUmvABZFljoOS75948S6hjlU0xU,54064
91
+ trilogy/parsing/parse_engine.py,sha256=32_yO_SreTjHxCkMziW2re15ilEZn01OUizVAvN9xHo,54656
92
92
  trilogy/parsing/render.py,sha256=o_XuQWhcwx1lD9eGVqkqZEwkmQK0HdmWWokGBtdeH4I,17837
93
93
  trilogy/parsing/trilogy.lark,sha256=EazfEvYPuvkPkNjUnVzFi0uD9baavugbSI8CyfawShk,12573
94
94
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
95
95
  trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
96
- pytrilogy-0.0.3.4.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
97
- pytrilogy-0.0.3.4.dist-info/METADATA,sha256=qxTMW9Dh0nhD4Ea42IlNy2tRWVPp54EDGBcjDmvlaZM,8983
98
- pytrilogy-0.0.3.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
99
- pytrilogy-0.0.3.4.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
100
- pytrilogy-0.0.3.4.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
101
- pytrilogy-0.0.3.4.dist-info/RECORD,,
96
+ pytrilogy-0.0.3.6.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
97
+ pytrilogy-0.0.3.6.dist-info/METADATA,sha256=PBxZLl7AH82ztlgGbJbdLWwj8r3Wo2_JRsXxgh5y1Gc,8983
98
+ pytrilogy-0.0.3.6.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
99
+ pytrilogy-0.0.3.6.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
100
+ pytrilogy-0.0.3.6.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
101
+ pytrilogy-0.0.3.6.dist-info/RECORD,,
trilogy/__init__.py CHANGED
@@ -4,6 +4,6 @@ from trilogy.dialect.enums import Dialects
4
4
  from trilogy.executor import Executor
5
5
  from trilogy.parser import parse
6
6
 
7
- __version__ = "0.0.3.4"
7
+ __version__ = "0.0.3.6"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -166,10 +166,8 @@ class UndefinedConcept(ConceptRef):
166
166
 
167
167
 
168
168
  def address_with_namespace(address: str, namespace: str) -> str:
169
- ns = address.split(".", 1)[0]
170
- if ns == namespace:
171
- return address
172
- if ns == DEFAULT_NAMESPACE:
169
+ existing_ns = address.split(".", 1)[0]
170
+ if existing_ns == DEFAULT_NAMESPACE:
173
171
  return f"{namespace}.{address.split('.',1)[1]}"
174
172
  return f"{namespace}.{address}"
175
173
 
@@ -203,7 +201,7 @@ class Parenthetical(
203
201
  def __repr__(self):
204
202
  return f"({str(self.content)})"
205
203
 
206
- def with_namespace(self, namespace: str):
204
+ def with_namespace(self, namespace: str) -> Parenthetical:
207
205
  return Parenthetical.model_construct(
208
206
  content=(
209
207
  self.content.with_namespace(namespace)
@@ -917,8 +915,6 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
917
915
  return self.name.replace(".", "_")
918
916
 
919
917
  def with_namespace(self, namespace: str) -> Self:
920
- if namespace == self.namespace:
921
- return self
922
918
  return self.__class__.model_construct(
923
919
  name=self.name,
924
920
  datatype=self.datatype,
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
  from typing import (
9
9
  TYPE_CHECKING,
10
10
  Annotated,
11
+ Any,
11
12
  Dict,
12
13
  ItemsView,
13
14
  List,
@@ -383,14 +384,13 @@ class Environment(BaseModel):
383
384
  ):
384
385
  exists = True
385
386
  imp_stm = Import(alias=alias, path=Path(source.working_path))
386
- same_namespace = alias == self.namespace
387
+ same_namespace = alias == DEFAULT_NAMESPACE
387
388
 
388
389
  if not exists:
389
390
  self.imports[alias].append(imp_stm)
390
391
  # we can't exit early
391
392
  # as there may be new concepts
392
393
  for k, concept in source.concepts.items():
393
-
394
394
  # skip internal namespace
395
395
  if INTERNAL_NAMESPACE in concept.address:
396
396
  continue
@@ -416,7 +416,6 @@ class Environment(BaseModel):
416
416
  self.alias_origin_lookup[address_with_namespace(key, alias)] = (
417
417
  val.with_namespace(alias)
418
418
  )
419
-
420
419
  return self
421
420
 
422
421
  def add_file_import(
@@ -427,7 +426,6 @@ class Environment(BaseModel):
427
426
  from trilogy.parsing.parse_engine import (
428
427
  PARSER,
429
428
  ParseToObjects,
430
- gen_cache_lookup,
431
429
  )
432
430
 
433
431
  if isinstance(path, str):
@@ -441,7 +439,8 @@ class Environment(BaseModel):
441
439
  else:
442
440
  target = path
443
441
  if not env:
444
- parse_address = gen_cache_lookup(str(target), alias, str(self.working_path))
442
+ import_keys = ["root", alias]
443
+ parse_address = "-".join(import_keys)
445
444
  try:
446
445
  with open(target, "r", encoding="utf-8") as f:
447
446
  text = f.read()
@@ -455,10 +454,13 @@ class Environment(BaseModel):
455
454
  ),
456
455
  parse_address=parse_address,
457
456
  token_address=target,
457
+ import_keys=import_keys,
458
458
  )
459
459
  nparser.set_text(text)
460
+ nparser.environment.concepts.fail_on_missing = False
460
461
  nparser.transform(PARSER.parse(text))
461
- nparser.hydrate_missing()
462
+ nparser.run_second_parse_pass()
463
+ nparser.environment.concepts.fail_on_missing = True
462
464
 
463
465
  except Exception as e:
464
466
  raise ImportError(
@@ -664,37 +666,59 @@ class LazyEnvironment(Environment):
664
666
  until relevant attributes accessed."""
665
667
 
666
668
  load_path: Path
669
+ setup_queries: list[Any] = Field(default_factory=list)
667
670
  loaded: bool = False
668
671
 
672
+ @property
673
+ def setup_path(self) -> Path:
674
+ return self.load_path.parent / "setup.preql"
675
+
669
676
  def __init__(self, **data):
677
+ if not data.get("working_path"):
678
+ data["working_path"] = data["load_path"].parent
670
679
  super().__init__(**data)
680
+ assert self.working_path == self.load_path.parent
671
681
 
672
682
  def _add_path_concepts(self):
673
683
  pass
674
684
 
685
+ def _load(self):
686
+ if self.loaded:
687
+ return
688
+ from trilogy import parse
689
+
690
+ env = Environment(working_path=self.load_path.parent)
691
+ assert env.working_path == self.load_path.parent
692
+ with open(self.load_path, "r") as f:
693
+ env, _ = parse(f.read(), env)
694
+ if self.setup_path.exists():
695
+ with open(self.setup_path, "r") as f2:
696
+ env, q = parse(f2.read(), env)
697
+ for q in q:
698
+ self.setup_queries.append(q)
699
+ self.loaded = True
700
+ self.datasources = env.datasources
701
+ self.concepts = env.concepts
702
+ self.imports = env.imports
703
+ self.alias_origin_lookup = env.alias_origin_lookup
704
+ self.materialized_concepts = env.materialized_concepts
705
+ self.functions = env.functions
706
+ self.data_types = env.data_types
707
+ self.cte_name_map = env.cte_name_map
708
+
675
709
  def __getattribute__(self, name):
676
- if name in (
677
- "load_path",
678
- "loaded",
679
- "working_path",
680
- "model_config",
681
- "model_fields",
682
- "model_post_init",
710
+ if name not in (
711
+ "datasources",
712
+ "concepts",
713
+ "imports",
714
+ "materialized_concepts",
715
+ "functions",
716
+ "datatypes",
717
+ "cte_name_map",
683
718
  ) or name.startswith("_"):
684
719
  return super().__getattribute__(name)
685
720
  if not self.loaded:
686
- logger.info(
687
- f"lazily evaluating load path {self.load_path} to access {name}"
688
- )
689
- from trilogy import parse
690
-
691
- env = Environment(working_path=str(self.working_path))
692
- with open(self.load_path, "r") as f:
693
- parse(f.read(), env)
694
- self.loaded = True
695
- self.datasources = env.datasources
696
- self.concepts = env.concepts
697
- self.imports = env.imports
721
+ self._load()
698
722
  return super().__getattribute__(name)
699
723
 
700
724
 
@@ -1046,7 +1046,7 @@ def source_query_concepts(
1046
1046
  f"{c.address}<{c.purpose}>{c.derivation}>" for c in output_concepts
1047
1047
  ]
1048
1048
  raise ValueError(
1049
- f"Could not resolve conections between {error_strings} from environment graph."
1049
+ f"Could not resolve connections between {error_strings} from environment graph."
1050
1050
  )
1051
1051
  final = [x for x in root.output_concepts if x.address not in root.hidden_concepts]
1052
1052
  logger.info(
@@ -385,7 +385,7 @@ def get_query_node(
385
385
  )
386
386
  graph = generate_graph(build_environment)
387
387
  logger.info(
388
- f"{LOGGER_PREFIX} getting source datasource for outputs {statement.output_components} grain {build_statement.grain}"
388
+ f"{LOGGER_PREFIX} getting source datasource for outputs {build_statement.output_components} grain {build_statement.grain}"
389
389
  )
390
390
 
391
391
  search_concepts: list[BuildConcept] = build_statement.output_components
trilogy/executor.py CHANGED
@@ -346,10 +346,10 @@ class Executor(object):
346
346
  file = Path(file)
347
347
  with open(file, "r") as f:
348
348
  command = f.read()
349
- return self.parse_text_generator(command, persist=persist)
349
+ return self.parse_text_generator(command, persist=persist, root=file)
350
350
 
351
351
  def parse_text(
352
- self, command: str, persist: bool = False
352
+ self, command: str, persist: bool = False, root: Path | None = None
353
353
  ) -> List[
354
354
  ProcessedQuery
355
355
  | ProcessedQueryPersist
@@ -357,9 +357,11 @@ class Executor(object):
357
357
  | ProcessedRawSQLStatement
358
358
  | ProcessedCopyStatement
359
359
  ]:
360
- return list(self.parse_text_generator(command, persist=persist))
360
+ return list(self.parse_text_generator(command, persist=persist, root=root))
361
361
 
362
- def parse_text_generator(self, command: str, persist: bool = False) -> Generator[
362
+ def parse_text_generator(
363
+ self, command: str, persist: bool = False, root: Path | None = None
364
+ ) -> Generator[
363
365
  ProcessedQuery
364
366
  | ProcessedQueryPersist
365
367
  | ProcessedShowStatement
@@ -369,7 +371,7 @@ class Executor(object):
369
371
  None,
370
372
  ]:
371
373
  """Process a preql text command"""
372
- _, parsed = parse_text(command, self.environment)
374
+ _, parsed = parse_text(command, self.environment, root=root)
373
375
  generatable = [
374
376
  x
375
377
  for x in parsed
@@ -129,6 +129,8 @@ CONSTANT_TYPES = (int, float, str, bool, list, ListWrapper, MapWrapper)
129
129
 
130
130
  SELF_LABEL = "root"
131
131
 
132
+ MAX_PARSE_DEPTH = 10
133
+
132
134
 
133
135
  @dataclass
134
136
  class WholeGrainWrapper:
@@ -146,10 +148,6 @@ with open(join(dirname(__file__), "trilogy.lark"), "r") as f:
146
148
  )
147
149
 
148
150
 
149
- def gen_cache_lookup(path: str, alias: str, parent: str) -> str:
150
- return path + alias + parent
151
-
152
-
153
151
  def parse_concept_reference(
154
152
  name: str, environment: Environment, purpose: Optional[Purpose] = None
155
153
  ) -> Tuple[str, str, str, str | None]:
@@ -223,6 +221,8 @@ class ParseToObjects(Transformer):
223
221
  parsed: dict[str, "ParseToObjects"] | None = None,
224
222
  tokens: dict[Path | str, ParseTree] | None = None,
225
223
  text_lookup: dict[Path | str, str] | None = None,
224
+ environment_lookup: dict[str, Environment] | None = None,
225
+ import_keys: list[str] | None = None,
226
226
  ):
227
227
  Transformer.__init__(self, True)
228
228
  self.environment: Environment = environment
@@ -230,6 +230,7 @@ class ParseToObjects(Transformer):
230
230
  self.token_address: Path | str = token_address or SELF_LABEL
231
231
  self.parsed: dict[str, ParseToObjects] = parsed if parsed is not None else {}
232
232
  self.tokens: dict[Path | str, ParseTree] = tokens if tokens is not None else {}
233
+ self.environments: dict[str, Environment] = environment_lookup or {}
233
234
  self.text_lookup: dict[Path | str, str] = (
234
235
  text_lookup if text_lookup is not None else {}
235
236
  )
@@ -237,6 +238,7 @@ class ParseToObjects(Transformer):
237
238
  # after initial parsing
238
239
  self.parse_pass = ParsePass.INITIAL
239
240
  self.function_factory = FunctionFactory(self.environment)
241
+ self.import_keys: list[str] = import_keys or ["root"]
240
242
 
241
243
  def set_text(self, text: str):
242
244
  self.text_lookup[self.token_address] = text
@@ -252,12 +254,14 @@ class ParseToObjects(Transformer):
252
254
  for _, v in self.parsed.items():
253
255
  v.prepare_parse()
254
256
 
255
- def hydrate_missing(self):
257
+ def run_second_parse_pass(self, force: bool = False):
258
+ if self.token_address not in self.tokens:
259
+ return []
256
260
  self.parse_pass = ParsePass.VALIDATION
257
- for k, v in self.parsed.items():
261
+ for _, v in list(self.parsed.items()):
258
262
  if v.parse_pass == ParsePass.VALIDATION:
259
263
  continue
260
- v.hydrate_missing()
264
+ v.run_second_parse_pass()
261
265
  reparsed = self.transform(self.tokens[self.token_address])
262
266
  self.environment.concepts.undefined = {}
263
267
  return reparsed
@@ -301,11 +305,6 @@ class ParseToObjects(Transformer):
301
305
  def QUOTED_IDENTIFIER(self, args) -> str:
302
306
  return args.value[1:-1]
303
307
 
304
- # @v_args(meta=True)
305
- # def concept_lit(self, meta: Meta, args) -> ConceptRef:
306
- # address = args[0]
307
- # return self.environment.concepts.__getitem__(address, meta.line)
308
- # return ConceptRef(address=address, line_no=meta.line)
309
308
  @v_args(meta=True)
310
309
  def concept_lit(self, meta: Meta, args) -> ConceptRef:
311
310
  address = args[0]
@@ -390,7 +389,6 @@ class ParseToObjects(Transformer):
390
389
 
391
390
  @v_args(meta=True)
392
391
  def column_assignment(self, meta: Meta, args):
393
- # TODO -> deal with conceptual modifiers
394
392
  modifiers = []
395
393
  alias = args[0]
396
394
  concept_list = args[1]
@@ -853,8 +851,10 @@ class ParseToObjects(Transformer):
853
851
  def import_statement(self, args: list[str]) -> ImportStatement:
854
852
  if len(args) == 2:
855
853
  alias = args[-1]
854
+ cache_key = args[-1]
856
855
  else:
857
856
  alias = self.environment.namespace
857
+ cache_key = args[0]
858
858
  path = args[0].split(".")
859
859
 
860
860
  target = join(self.environment.working_path, *path) + ".preql"
@@ -862,10 +862,14 @@ class ParseToObjects(Transformer):
862
862
  # tokens + text are cached by path
863
863
  token_lookup = Path(target)
864
864
 
865
- # cache lookups by the target, the alias, and the file we're importing it from
866
- cache_lookup = gen_cache_lookup(
867
- path=target, alias=alias, parent=str(self.token_address)
868
- )
865
+ # parser + env has to be cached by prior import path + current key
866
+ key_path = self.import_keys + [cache_key]
867
+ cache_lookup = "-".join(key_path)
868
+
869
+ # we don't iterate past the max parse depth
870
+ if len(key_path) > MAX_PARSE_DEPTH:
871
+ return ImportStatement(alias=alias, path=Path(target))
872
+
869
873
  if token_lookup in self.tokens:
870
874
  raw_tokens = self.tokens[token_lookup]
871
875
  text = self.text_lookup[token_lookup]
@@ -879,10 +883,14 @@ class ParseToObjects(Transformer):
879
883
  if cache_lookup in self.parsed:
880
884
  nparser = self.parsed[cache_lookup]
881
885
  new_env = nparser.environment
886
+ if nparser.parse_pass != ParsePass.VALIDATION:
887
+ # nparser.transform(raw_tokens)
888
+ nparser.run_second_parse_pass()
882
889
  else:
883
890
  try:
884
891
  new_env = Environment(
885
892
  working_path=dirname(target),
893
+ env_file_path=token_lookup,
886
894
  )
887
895
  new_env.concepts.fail_on_missing = False
888
896
  self.parsed[self.parse_address] = self
@@ -893,6 +901,7 @@ class ParseToObjects(Transformer):
893
901
  parsed=self.parsed,
894
902
  tokens=self.tokens,
895
903
  text_lookup=self.text_lookup,
904
+ import_keys=self.import_keys + [cache_key],
896
905
  )
897
906
  nparser.transform(raw_tokens)
898
907
  self.parsed[cache_lookup] = nparser
@@ -901,9 +910,11 @@ class ParseToObjects(Transformer):
901
910
  f"Unable to import file {target}, parsing error: {e}"
902
911
  ) from e
903
912
 
904
- imps = ImportStatement(alias=alias, path=Path(args[0]))
913
+ parsed_path = Path(args[0])
914
+ imps = ImportStatement(alias=alias, path=parsed_path)
915
+
905
916
  self.environment.add_import(
906
- alias, new_env, Import(alias=alias, path=Path(args[0]))
917
+ alias, new_env, Import(alias=alias, path=parsed_path)
907
918
  )
908
919
  return imps
909
920
 
@@ -1594,7 +1605,9 @@ def parse_text_raw(text: str, environment: Optional[Environment] = None):
1594
1605
  PARSER.parse(text)
1595
1606
 
1596
1607
 
1597
- def parse_text(text: str, environment: Optional[Environment] = None) -> Tuple[
1608
+ def parse_text(
1609
+ text: str, environment: Optional[Environment] = None, root: Path | None = None
1610
+ ) -> Tuple[
1598
1611
  Environment,
1599
1612
  List[
1600
1613
  Datasource
@@ -1606,8 +1619,10 @@ def parse_text(text: str, environment: Optional[Environment] = None) -> Tuple[
1606
1619
  | None
1607
1620
  ],
1608
1621
  ]:
1609
- environment = environment or Environment()
1610
- parser = ParseToObjects(environment=environment)
1622
+ environment = environment or (
1623
+ Environment(working_path=root) if root else Environment()
1624
+ )
1625
+ parser = ParseToObjects(environment=environment, import_keys=["root"])
1611
1626
 
1612
1627
  try:
1613
1628
  parser.set_text(text)
@@ -1615,7 +1630,7 @@ def parse_text(text: str, environment: Optional[Environment] = None) -> Tuple[
1615
1630
  parser.prepare_parse()
1616
1631
  parser.transform(PARSER.parse(text))
1617
1632
  # this will reset fail on missing
1618
- pass_two = parser.hydrate_missing()
1633
+ pass_two = parser.run_second_parse_pass()
1619
1634
  output = [v for v in pass_two if v]
1620
1635
  environment.concepts.fail_on_missing = True
1621
1636
  except VisitError as e: