Skip to content

Commit a7c5824

Browse files
authored
Merge pull request #93 from wimglenn/stack
be more strict when crawling the stack
2 parents e66f0fe + edfc9f6 commit a7c5824

9 files changed

+92
-22
lines changed

.github/workflows/tests.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
strategy:
1616
matrix:
1717
os:
18-
- ubuntu-latest
18+
- ubuntu-20.04
1919
- macos-latest
2020
- windows-latest
2121
python-version:

README.rst

+6
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ Install with pip
6161
6262
pip install advent-of-code-data
6363
64+
If you want to use this within a Jupyter notebook, there are some extra deps:
65+
66+
.. code-block:: bash
67+
68+
pip install 'advent-of-code-data[nb]'
69+
6470
**Puzzle inputs differ by user.** So export your session ID, for example:
6571

6672
.. code-block:: bash

aocd/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from . import transforms
1818
from . import utils
1919
from . import version
20+
from . import _ipykernel
2021
from .exceptions import AocdError
2122
from .exceptions import PuzzleUnsolvedError
2223
from .get import get_data
@@ -44,6 +45,7 @@
4445
"AocdError",
4546
"PuzzleUnsolvedError",
4647
"AOC_TZ",
48+
"_ipykernel",
4749
]
4850
__all__ += transforms.__all__
4951

aocd/_ipykernel.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import os
2+
import re
3+
4+
import requests
5+
6+
7+
def get_ipynb_path():
8+
# inline imports to avoid hard-dependency on IPython/jupyter
9+
import IPython
10+
from jupyter_server import serverapp
11+
from jupyter_server.utils import url_path_join
12+
app = IPython.get_ipython().config["IPKernelApp"]
13+
kernel_id = re.search(r"(?<=kernel-)[\w\-]+(?=\.json)", app["connection_file"])[0]
14+
for serv in serverapp.list_running_servers():
15+
url = url_path_join(serv["url"], "api/sessions")
16+
resp = requests.get(url, params={"token": serv["token"]})
17+
resp.raise_for_status()
18+
for sess in resp.json():
19+
if kernel_id == sess["kernel"]["id"]:
20+
path = serv["root_dir"]
21+
fname = sess["notebook"]["path"]
22+
return os.path.join(path, fname)

aocd/get.py

+26-12
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from .models import User
1818
from .utils import AOC_TZ
1919
from .utils import blocker
20+
from ._ipykernel import get_ipynb_path
2021

2122

2223
log = getLogger(__name__)
@@ -96,23 +97,36 @@ def get_day_and_year():
9697
"""
9798
pattern_year = r"201[5-9]|202[0-9]"
9899
pattern_day = r"2[0-5]|1[0-9]|[1-9]"
99-
stack = [f[0] for f in traceback.extract_stack()]
100-
for name in stack:
101-
basename = os.path.basename(name)
100+
for frame in traceback.extract_stack():
101+
filename = frame[0]
102+
linetxt = frame[-1] or ""
103+
basename = os.path.basename(filename)
102104
reasons_to_skip_frame = [
103105
not re.search(pattern_day, basename), # no digits in filename
104-
name == __file__, # here
105-
"importlib" in name, # Python 3 import machinery
106-
"/IPython/" in name, # IPython adds a tonne of stack frames
107-
name.startswith("<"), # crap like <decorator-gen-57>
108-
name.endswith("ython3"), # ipython3 alias
106+
filename == __file__, # here
107+
"importlib" in filename, # Python 3 import machinery
108+
"/IPython/" in filename, # IPython adds a tonne of stack frames
109+
filename.startswith("<"), # crap like <decorator-gen-57>
110+
filename.endswith("ython3"), # ipython3 alias
109111
basename.startswith("pydev_ipython_console"), # PyCharm Python Console
112+
"aocd" not in linetxt,
113+
"ipykernel" in filename,
110114
]
111115
if not any(reasons_to_skip_frame):
112-
log.debug("stack crawl found %s", name)
113-
abspath = os.path.abspath(name)
116+
log.debug("stack crawl found %s", filename)
117+
abspath = os.path.abspath(filename)
114118
break
115-
log.debug("skipping frame %s", name)
119+
elif "ipykernel" in filename:
120+
log.debug("stack crawl found %s, attempting to detect an .ipynb", filename)
121+
try:
122+
abspath = get_ipynb_path()
123+
except Exception as err:
124+
log.debug("failed getting .ipynb path with %s %s", type(err), err)
125+
else:
126+
if abspath and re.search(pattern_day, abspath):
127+
basename = os.path.basename(abspath)
128+
break
129+
log.debug("skipping frame %s", filename)
116130
else:
117131
import __main__
118132
if getattr(__main__, "__file__", "<input>") == "<input>":
@@ -135,7 +149,7 @@ def get_day_and_year():
135149
assert not day.startswith("0"), "regex pattern_day must prevent any leading 0"
136150
day = int(day)
137151
assert 1 <= day <= 25, "regex pattern_day must only match numbers in range 1-25"
138-
log.debug("year=%d day=%d", year, day)
152+
log.debug("year=%s day=%s", year or "?", day)
139153
return day, year
140154
log.debug("giving up introspection for %s", abspath)
141155
raise AocdError("Failed introspection of day")

aocd/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.2.3"
1+
__version__ = "1.3.0"

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,5 @@
4545
"tzlocal",
4646
],
4747
options={"bdist_wheel": {"universal": "1"}},
48+
extras_require={"nb": ["IPython", "jupyter-server"]}
4849
)

tests/test_aocd.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def test_get_data_imported_from_class():
99

1010

1111
def test_get_data_via_import(mocker):
12-
fake_stack = [("~/2017/q23.py",)]
12+
fake_stack = [("~/2017/q23.py", 1, "<test>", "from aocd import data")]
1313
mocker.patch("aocd.get.traceback.extract_stack", return_value=fake_stack)
1414
mock = mocker.patch("aocd._module.get_data", return_value="test data")
1515
from aocd import data
@@ -19,7 +19,7 @@ def test_get_data_via_import(mocker):
1919

2020

2121
def test_import_submit_binds_day_and_year(mocker):
22-
fake_stack = [("~/2017/q23.py",)]
22+
fake_stack = [("~/2017/q23.py", 1, "<test>", "from aocd import data")]
2323
mocker.patch("aocd.get.traceback.extract_stack", return_value=fake_stack)
2424
from aocd import submit
2525

@@ -46,7 +46,7 @@ def test_get_data_via_import_in_interactive_mode(monkeypatch, mocker, freezer):
4646

4747

4848
def test_get_lines_via_import(mocker):
49-
fake_stack = [("~/2017/q23.py",)]
49+
fake_stack = [("~/2017/q23.py", 1, "<test>", "from aocd import data")]
5050
mocker.patch("aocd.get.traceback.extract_stack", return_value=fake_stack)
5151
mock = mocker.patch("aocd._module.get_data", return_value="line 1\nline 2\nline 3")
5252
from aocd import lines
@@ -56,7 +56,7 @@ def test_get_lines_via_import(mocker):
5656

5757

5858
def test_get_numbers_via_import(mocker):
59-
fake_stack = [("~/2017/q23.py",)]
59+
fake_stack = [("~/2017/q23.py", 1, "<test>", "from aocd import data")]
6060
mocker.patch("aocd.get.traceback.extract_stack", return_value=fake_stack)
6161
mock = mocker.patch("aocd._module.get_data", return_value="1\n2\n3")
6262
from aocd import numbers

tests/test_date_introspection.py

+29-4
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,54 @@ def test_get_day_and_year_fail_no_filename_on_stack():
1010

1111

1212
def test_get_day_and_year_from_stack(mocker):
13-
fake_stack = [("xmas_problem_2016_25b_dawg.py",)]
13+
fake_stack = [("xmas_problem_2016_25b_dawg.py", 1, "<test>", "from aocd import data")]
1414
mocker.patch("aocd.get.traceback.extract_stack", return_value=fake_stack)
1515
day, year = get_day_and_year()
1616
assert day == 25
1717
assert year == 2016
1818

1919

2020
def test_year_is_ambiguous(mocker):
21-
fake_stack = [("~/2016/2017_q01.py",)]
21+
fake_stack = [("~/2016/2017_q01.py", 1, "<test>", "from aocd import data")]
2222
mocker.patch("aocd.get.traceback.extract_stack", return_value=fake_stack)
2323
with pytest.raises(AocdError("Failed introspection of year")):
2424
get_day_and_year()
2525

2626

2727
def test_day_is_unknown(mocker):
28-
fake_stack = [("~/2016.py",)]
28+
fake_stack = [("~/2016.py", 1, "<test>", "from aocd import data")]
2929
mocker.patch("aocd.get.traceback.extract_stack", return_value=fake_stack)
3030
with pytest.raises(AocdError("Failed introspection of day")):
3131
get_day_and_year()
3232

3333

3434
def test_day_is_invalid(mocker):
35-
fake_stack = [("~/2016/q27.py",)]
35+
fake_stack = [("~/2016/q27.py", 1, "<test>", "from aocd import data")]
3636
mocker.patch("aocd.get.traceback.extract_stack", return_value=fake_stack)
3737
with pytest.raises(AocdError("Failed introspection of day")):
3838
get_day_and_year()
39+
40+
41+
def test_ipynb_ok(mocker):
42+
fake_stack = [("ipykernel/123456789.py", 1, "<test>", "from aocd import data")]
43+
mocker.patch("aocd.get.traceback.extract_stack", return_value=fake_stack)
44+
mocker.patch("aocd.get.get_ipynb_path", return_value="puzzle-2020-03.py")
45+
day, year = get_day_and_year()
46+
assert day == 3
47+
assert year == 2020
48+
49+
50+
def test_ipynb_fail(mocker):
51+
fake_stack = [("ipykernel/123456789.py", 1, "<test>", "from aocd import data")]
52+
mocker.patch("aocd.get.traceback.extract_stack", return_value=fake_stack)
53+
mocker.patch("aocd.get.get_ipynb_path", side_effect=ImportError)
54+
with pytest.raises(AocdError("Failed introspection of filename")):
55+
get_day_and_year()
56+
57+
58+
def test_ipynb_fail_no_numbers_in_ipynb_filename(mocker):
59+
fake_stack = [("ipykernel/123456789.py", 1, "<test>", "from aocd import data")]
60+
mocker.patch("aocd.get.traceback.extract_stack", return_value=fake_stack)
61+
mocker.patch("aocd.get.get_ipynb_path", "puzzle.py")
62+
with pytest.raises(AocdError("Failed introspection of filename")):
63+
get_day_and_year()

0 commit comments

Comments
 (0)