# -*- coding: utf-8 -*- """Where the PLANT knowledge layer lives on disk. Split out of `designspec` for one reason: `designspec` imports the Revit API at module scope (evidence scraping needs a live document), and choosing the plant knowledge file is pure path arithmetic that a host CPython test can and should exercise directly. `designspec` re-exports every name here, so nothing that already says `designspec.plant_knowledge_path()` had to change. THE LAYOUT, in one line: /knowledge.yaml the machine-wide plant file //knowledge.yaml one folder per plant A job's own knowledge file names its plant in `job.plant`; `knowledge.load()` reads that name BEFORE it merges anything and passes it here. A job that names no plant reads the machine-wide file, which is how every install worked before per-plant folders existed and still does. IronPython 2.7: no f-strings, no pathlib. """ # pylint: disable=invalid-name import os import re from vibe.compat import string_types # The plant-wide knowledge file: one per machine, above every project. PLANT_FOLDER = 'WillTools' PLANT_ENV = 'WILLTOOLS_KNOWLEDGE' # What a plant id may be made of, because it is used as a PATH COMPONENT. # It arrives from a YAML file, so `..`, a slash or a drive letter in it would # be a directory traversal written by whoever edits a job file. Anything # outside this set is refused outright rather than scrubbed into something # else: a plant id that quietly became a different folder name is exactly the # silence this codebase keeps paying for. _PLANT_ID_RE = re.compile(r'^[A-Za-z0-9][A-Za-z0-9._-]*$') def valid_plant_id(plant): """Is this string usable as a plant id, i.e. as one folder name? Returns False for None, for the empty string, and for anything carrying a separator, a drive letter or a dot-dot. See _PLANT_ID_RE. """ if not plant or not isinstance(plant, string_types): return False return bool(_PLANT_ID_RE.match(plant.strip())) def plant_root(): """The folder the plant knowledge lives in -- one file, or one per plant. THE ROOT IS A FOLDER AND THE MACHINE-WIDE FILE IS `knowledge.yaml` INSIDE IT. That relationship has always held (%APPDATA%/WillTools/knowledge.yaml) and is what lets WILLTOOLS_KNOWLEDGE keep meaning what it always meant while also naming a root: set to a FOLDER that folder is the root. Nothing else to say. set to a FILE that file is the machine-wide plant file, so its DIRECTORY is the root, by the same relationship. Not a guess -- the definition, read backwards. unset %APPDATA%/WillTools, as ever. Which is why a machine already pointing the variable at a single file picks up per-plant folders with no change to the variable: the folders are siblings of the file it already names. A path that is set but exists as neither is treated as a FILE, because that is what the variable has meant for its whole life and the operator almost certainly meant a file that has moved -- the missing path is then reported by name instead of a different, existing path being read in silence. """ override = os.environ.get(PLANT_ENV) if not override: base = os.environ.get('APPDATA') or os.path.expanduser('~') return os.path.join(base, PLANT_FOLDER) if os.path.isdir(override): return override return os.path.dirname(override) or override def plant_knowledge_path(plant=None): """The plant knowledge file this run should read. No plant named -- the ordinary case, and every install that predates the key -- returns the machine-wide file exactly as this function always has: WILLTOOLS_KNOWLEDGE if it names one, else /knowledge.yaml. A plant named (`job.plant` in the job layer) returns //knowledge.yaml. A SECOND PLANT IS A NEW FOLDER, never an edit to the first one's file. See plant_root() for what is and why a machine already pointing the variable at a single file needs no change. The per-plant FOLDER and the machine-wide FILE share a root and cannot collide: one is a directory, the other is called `knowledge.yaml`. An INVALID plant id -- a separator, a `..`, an empty string -- resolves to NOTHING and returns None. load() reports it. Scrubbing a malformed name into some nearby folder is how a run ends up on another plant's rules while looking entirely normal. """ if plant is None: override = os.environ.get(PLANT_ENV) if override and not os.path.isdir(override): return override # the single-file arrangement, untouched return os.path.join(plant_root(), 'knowledge.yaml') if not valid_plant_id(plant): return None return os.path.join(plant_root(), plant.strip(), 'knowledge.yaml')