Repository: wimglenn/advent-of-code-data

Issue:
No name "Puzzle" in module "aocd"
I installed this module on python3.10 with pip and `get_data` works correctly, but `Puzzle` (along with `lines` and `numbers`) is not recognized as part of the module. What could be causing this error?

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/wimglenn/advent-of-code-data/pull/96?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wim+glenn) Report
> Merging [#96](https://codecov.io/gh/wimglenn/advent-of-code-data/pull/96?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wim+glenn) (1953b17) into [main](https://codecov.io/gh/wimglenn/advent-of-code-data/commit/a7c5824e01273e3e8daddcd4bb8fcb094c6a431d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wim+glenn) (a7c5824) will **decrease** coverage by `0.47%`.
> The diff coverage is `93.10%`.

```diff
@@            Coverage Diff             @@
##             main      #96      +/-   ##
==========================================
- Coverage   92.81%   92.34%   -0.48%     
==========================================
  Files          25       25              
  Lines        2143     2168      +25     
  Branches      272      279       +7     
==========================================
+ Hits         1989     2002      +13     
- Misses        125      134       +9     
- Partials       29       32       +3     
```


| [Impacted Files](https://codecov.io/gh/wimglenn/advent-of-code-data/pull/96?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wim+glenn) | Coverage Δ | |
|---|---|---|
| [aocd/get.py](https://codecov.io/gh/wimglenn/advent-of-code-data/pull/96/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wim+glenn#diff-YW9jZC9nZXQucHk=) | `95.72% <90.90%> (-1.22%)` | :arrow_down: |
| [aocd/version.py](https://codecov.io/gh/wimglenn/advent-of-code-data/pull/96/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wim+glenn#diff-YW9jZC92ZXJzaW9uLnB5) | `100.00% <100.00%> (ø)` | |
| [tests/test\_date\_introspection.py](https://codecov.io/gh/wimglenn/advent-of-code-data/pull/96/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wim+glenn#diff-dGVzdHMvdGVzdF9kYXRlX2ludHJvc3BlY3Rpb24ucHk=) | `100.00% <100.00%> (ø)` | |
| [aocd/\_\_init\_\_.py](https://codecov.io/gh/wimglenn/advent-of-code-data/pull/96/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wim+glenn#diff-YW9jZC9fX2luaXRfXy5weQ==) | `94.11% <0.00%> (-5.89%)` | :arrow_down: |
| [aocd/runner.py](https://codecov.io/gh/wimglenn/advent-of-code-data/pull/96/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wim+glenn#diff-YW9jZC9ydW5uZXIucHk=) | `92.39% <0.00%> (-2.72%)` | :arrow_down: |
| [aocd/cookies.py](https://codecov.io/gh/wimglenn/advent-of-code-data/pull/96/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wim+glenn#diff-YW9jZC9jb29raWVzLnB5) | `32.69% <0.00%> (-0.97%)` | :arrow_down: |
| [tests/test\_get\_data.py](https://codecov.io/gh/wimglenn/advent-of-code-data/pull/96/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wim+glenn#diff-dGVzdHMvdGVzdF9nZXRfZGF0YS5weQ==) | `99.10% <0.00%> (-0.90%)` | :arrow_down: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wim+glenn)


Patch:
diff --git a/aocd/get.py b/aocd/get.py
index aea410c..017e2f5 100644
--- a/aocd/get.py
+++ b/aocd/get.py
@@ -97,6 +97,15 @@ def get_day_and_year():
     """
     pattern_year = r"201[5-9]|202[0-9]"
     pattern_day = r"2[0-5]|1[0-9]|[1-9]"
+    pattern_path = re.escape(os.sep) + re.escape(os.sep).join([r"20\d\d", r"[0-2]?\d", r".*\.py$"])
+    visited = []
+
+    def giveup(msg):
+        log.info("introspection failure")
+        for fname in visited:
+            log.info("stack crawl visited %s", fname)
+        return AocdError(msg)
+
     for frame in traceback.extract_stack():
         filename = frame[0]
         linetxt = frame[-1] or ""
@@ -112,6 +121,7 @@ def get_day_and_year():
             "aocd" not in linetxt,
             "ipykernel" in filename,
         ]
+        visited.append(filename)
         if not any(reasons_to_skip_frame):
             log.debug("stack crawl found %s", filename)
             abspath = os.path.abspath(filename)
@@ -126,6 +136,18 @@ def get_day_and_year():
                 if abspath and re.search(pattern_day, abspath):
                     basename = os.path.basename(abspath)
                     break
+        elif re.search(pattern_path, filename):
+            year = day = None
+            for part in filename.split(os.sep):
+                if not part.isdigit():
+                    continue
+                if len(part) == 4:
+                    year = int(part)
+                elif 1 <= len(part) <= 2:
+                    day = int(part)
+            if year is not None and day is not None:
+                log.debug("year=%s day=%s filename=%s", year, day, filename)
+                return day, year
         log.debug("skipping frame %s", filename)
     else:
         import __main__
@@ -135,10 +157,10 @@ def get_day_and_year():
             year = most_recent_year()
             return day, year
         log.debug("non-interactive")
-        raise AocdError("Failed introspection of filename")
+        raise giveup("Failed introspection of filename")
     years = {int(year) for year in re.findall(pattern_year, abspath)}
     if len(years) > 1:
-        raise AocdError("Failed introspection of year")
+        raise giveup("Failed introspection of year")
     year = years.pop() if years else None
     basename_no_years = re.sub(pattern_year, "", basename)
     try:
@@ -152,4 +174,4 @@ def get_day_and_year():
         log.debug("year=%s day=%s", year or "?", day)
         return day, year
     log.debug("giving up introspection for %s", abspath)
-    raise AocdError("Failed introspection of day")
+    raise giveup("Failed introspection of day")
diff --git a/aocd/version.py b/aocd/version.py
index 67bc602..9c73af2 100644
--- a/aocd/version.py
+++ b/aocd/version.py
@@ -1,1 +1,1 @@
-__version__ = "1.3.0"
+__version__ = "1.3.1"


Test patch:
diff --git a/tests/test_date_introspection.py b/tests/test_date_introspection.py
index 6f2c17a..3f478ee 100644
--- a/tests/test_date_introspection.py
+++ b/tests/test_date_introspection.py
@@ -1,3 +1,5 @@
+import os
+
 import pytest
 
 from aocd.exceptions import AocdError
@@ -61,3 +63,12 @@ def test_ipynb_fail_no_numbers_in_ipynb_filename(mocker):
     mocker.patch("aocd.get.get_ipynb_path", "puzzle.py")
     with pytest.raises(AocdError("Failed introspection of filename")):
         get_day_and_year()
+
+
+def test_no_numbers_in_py_filename_but_date_in_abspath(mocker):
+    fname = os.sep.join(["adventofcode", "2022", "02", "main.py"])
+    fake_stack = [(fname, 1, "<test>", "from aocd import data")]
+    mocker.patch("aocd.get.traceback.extract_stack", return_value=fake_stack)
+    day, year = get_day_and_year()
+    assert day == 2
+    assert year == 2022