Skip to content

Commit 1e0e854

Browse files
authored
Fix execution on UNIX systems and release v0.0.3. (#4)
1 parent 7b797f2 commit 1e0e854

17 files changed

+178
-65
lines changed

.pre-commit-config.yaml

+9-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,20 @@ repos:
33
rev: v3.4.0
44
hooks:
55
- id: check-added-large-files
6-
args: ['--maxkb=100']
6+
args: ['--maxkb=25']
7+
- id: check-case-conflict
78
- id: check-merge-conflict
9+
- id: check-vcs-permalinks
810
- id: check-yaml
911
exclude: meta.yaml
1012
- id: debug-statements
1113
- id: end-of-file-fixer
14+
- id: fix-byte-order-marker
15+
- id: forbid-new-submodules
16+
- id: mixed-line-ending
17+
- id: no-commit-to-branch
18+
args: [--branch, main]
19+
- id: trailing-whitespace
1220
- repo: https://github.com/pre-commit/pygrep-hooks
1321
rev: v1.7.0 # Use the ref you want to point at
1422
hooks:

CHANGES.rst

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ chronological order. Releases follow `semantic versioning <https://semver.org/>`
66
all releases are available on `Anaconda.org <https://anaconda.org/pytask/pytask-stata>`_.
77

88

9+
0.0.3 - 2021-01-16
10+
------------------
11+
12+
- :gh:`4` removes log file handling on UNIX and raises an error if run in parallel.
13+
914
0.0.2 - 2020-10-30
1015
------------------
1116

README.rst

+16-7
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
.. image:: https://codecov.io/gh/pytask-dev/pytask-stata/branch/main/graph/badge.svg
1111
:target: https://codecov.io/gh/pytask-dev/pytask-stata
1212

13+
.. image:: https://results.pre-commit.ci/badge/github/pytask-dev/pytask-stata/main.svg
14+
:target: https://results.pre-commit.ci/latest/github/pytask-dev/pytask-stata/main
15+
:alt: pre-commit.ci status
16+
1317
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
1418
:target: https://github.com/psf/black
1519

@@ -57,6 +61,9 @@ Here is an example where you want to run ``script.do``.
5761
def task_run_do_file():
5862
pass
5963
64+
When executing a do-file, the current working directory changes to the directory of the
65+
script which is executed.
66+
6067

6168
Multiple dependencies and products
6269
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -103,8 +110,8 @@ for a ``"source"`` key in the dictionary and, secondly, under the key ``0``.
103110
Command Line Arguments
104111
~~~~~~~~~~~~~~~~~~~~~~
105112

106-
The decorator can be used to pass command line arguments to your Stata executable which
107-
is not done, by default, but you could pass the path of the product with
113+
The decorator can be used to pass command line arguments to your Stata executable. For
114+
example, pass the path of the product with
108115

109116
.. code-block:: python
110117
@@ -124,10 +131,12 @@ And in your ``script.do``, you can intercept the value with
124131
sysuse auto, clear
125132
save "`produces'"
126133
127-
Note that this solution only works if your current working directory is the same as the
128-
directory where the task file lives. It is because Stata does not swap directories. To
129-
make the task independent from the current working directory, pass the full path as an
130-
command line argument. Here is an example.
134+
The relative path inside the do-file works only because the pytask-stata switches the
135+
current working directory to the directory of the do-file before the task is executed.
136+
This is necessary precaution.
137+
138+
To make the task independent from the current working directory, pass the full path as
139+
an command line argument. Here is an example.
131140

132141
.. code-block:: python
133142
@@ -178,7 +187,7 @@ include the ``@pytask.mark.stata`` decorator in the parametrization just like wi
178187
Configuration
179188
-------------
180189

181-
pytask-stata offers new some new configuration values.
190+
pytask-stata can be configured with the following options.
182191

183192
stata_keep_log
184193
Use this option to keep the ``.log`` files which are produced for every task. This

environment.yml

+1
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ dependencies:
2424
- pytest-cov
2525
- pytest-xdist
2626
- tox-conda
27+
- virtualenv=20.0.33

setup.cfg

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[bumpversion]
2-
current_version = 0.0.2
2+
current_version = 0.0.3
33
parse = (?P<major>\d+)\.(?P<minor>\d+)(\.(?P<patch>\d+))(\-?((dev)?(?P<dev>\d+))?)
4-
serialize =
4+
serialize =
55
{major}.{minor}.{patch}dev{dev}
66
{major}.{minor}.{patch}
77

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
setup(
55
name="pytask-stata",
6-
version="0.0.2",
6+
version="0.0.3",
77
packages=find_packages(where="src"),
88
package_dir={"": "src"},
99
entry_points={"pytask": ["pytask_stata = pytask_stata.plugin"]},

src/pytask_stata/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.0.2"
1+
__version__ = "0.0.3"

src/pytask_stata/collect.py

+46-22
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
import copy
33
import functools
44
import subprocess
5-
from pathlib import Path
65
from typing import Iterable
76
from typing import Optional
7+
from typing import Sequence
88
from typing import Union
99

1010
from _pytask.config import hookimpl
@@ -14,6 +14,7 @@
1414
from _pytask.nodes import PythonFunctionTask
1515
from _pytask.parametrize import _copy_func
1616
from pytask_stata.shared import convert_task_id_to_name_of_log_file
17+
from pytask_stata.shared import get_node_from_dictionary
1718

1819

1920
def stata(options: Optional[Union[str, Iterable[str]]] = None):
@@ -25,16 +26,15 @@ def stata(options: Optional[Union[str, Iterable[str]]] = None):
2526
One or multiple command line options passed to Stata.
2627
2728
"""
28-
if options is None:
29-
options = []
30-
elif isinstance(options, str):
31-
options = [options]
29+
options = _to_list(options) if options is not None else []
30+
options = [str(i) for i in options]
3231
return options
3332

3433

35-
def run_stata_script(stata):
34+
def run_stata_script(stata, cwd):
3635
"""Run an R script."""
37-
subprocess.run(stata, check=True)
36+
print("Executing " + " ".join(stata) + ".") # noqa: T001
37+
subprocess.run(stata, cwd=cwd, check=True)
3838

3939

4040
@hookimpl
@@ -58,7 +58,7 @@ def pytask_collect_task(session, path, name, obj):
5858
def pytask_collect_task_teardown(session, task):
5959
"""Perform some checks and prepare the task function."""
6060
if get_specific_markers_from_task(task, "stata"):
61-
source = _get_node_from_dictionary(
61+
source = get_node_from_dictionary(
6262
task.depends_on, session.config["stata_source_key"]
6363
)
6464
if not (isinstance(source, FilePathNode) and source.value.suffix == ".do"):
@@ -72,19 +72,13 @@ def pytask_collect_task_teardown(session, task):
7272
merged_marks = _merge_all_markers(task)
7373
args = stata(*merged_marks.args, **merged_marks.kwargs)
7474
options = _prepare_cmd_options(session, task, args)
75-
stata_function = functools.partial(stata_function, stata=options)
75+
stata_function = functools.partial(
76+
stata_function, stata=options, cwd=task.path.parent
77+
)
7678

7779
task.function = stata_function
7880

7981

80-
def _get_node_from_dictionary(obj, key, fallback=0):
81-
if isinstance(obj, Path):
82-
pass
83-
elif isinstance(obj, dict):
84-
obj = obj.get(key) or obj.get(fallback)
85-
return obj
86-
87-
8882
def _merge_all_markers(task):
8983
"""Combine all information from markers for the Stata function."""
9084
stata_marks = get_specific_markers_from_task(task, "stata")
@@ -101,15 +95,45 @@ def _prepare_cmd_options(session, task, args):
10195
is unique and does not cause any errors when parallelizing the execution.
10296
10397
"""
104-
source = _get_node_from_dictionary(
98+
source = get_node_from_dictionary(
10599
task.depends_on, session.config["stata_source_key"]
106100
)
107-
log_name = convert_task_id_to_name_of_log_file(task.name)
108-
return [
101+
102+
cmd_options = [
109103
session.config["stata"],
110104
"-e",
111105
"do",
112-
source.value.as_posix(),
106+
source.path.as_posix(),
113107
*args,
114-
f"-{log_name}",
115108
]
109+
if session.config["platform"] == "win32":
110+
log_name = convert_task_id_to_name_of_log_file(task.name)
111+
cmd_options.append(f"-{log_name}")
112+
113+
return cmd_options
114+
115+
116+
def _to_list(scalar_or_iter):
117+
"""Convert scalars and iterables to list.
118+
119+
Parameters
120+
----------
121+
scalar_or_iter : str or list
122+
123+
Returns
124+
-------
125+
list
126+
127+
Examples
128+
--------
129+
>>> _to_list("a")
130+
['a']
131+
>>> _to_list(["b"])
132+
['b']
133+
134+
"""
135+
return (
136+
[scalar_or_iter]
137+
if isinstance(scalar_or_iter, str) or not isinstance(scalar_or_iter, Sequence)
138+
else list(scalar_or_iter)
139+
)

src/pytask_stata/config.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Configure pytask."""
22
import shutil
3+
import sys
34

45
from _pytask.config import hookimpl
56
from _pytask.shared import convert_truthy_or_falsy_to_bool
@@ -11,6 +12,7 @@
1112
def pytask_parse_config(config, config_from_cli, config_from_file):
1213
"""Register the r marker."""
1314
config["markers"]["stata"] = "Tasks which are executed with Stata."
15+
config["platform"] = sys.platform
1416

1517
if config_from_file.get("stata"):
1618
config["stata"] = config_from_file["stata"]

src/pytask_stata/execute.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from _pytask.config import hookimpl
55
from _pytask.mark import get_specific_markers_from_task
66
from pytask_stata.shared import convert_task_id_to_name_of_log_file
7+
from pytask_stata.shared import get_node_from_dictionary
78
from pytask_stata.shared import STATA_COMMANDS
89

910

@@ -34,8 +35,14 @@ def pytask_execute_task_teardown(session, task):
3435
3536
"""
3637
if get_specific_markers_from_task(task, "stata"):
37-
log_name = convert_task_id_to_name_of_log_file(task.name)
38-
path_to_log = task.path.with_name(log_name).with_suffix(".log")
38+
if session.config["platform"] == "win32":
39+
log_name = convert_task_id_to_name_of_log_file(task.name)
40+
path_to_log = task.path.with_name(log_name).with_suffix(".log")
41+
else:
42+
source = get_node_from_dictionary(
43+
task.depends_on, session.config["stata_source_key"]
44+
)
45+
path_to_log = source.path.with_suffix(".log")
3946

4047
n_lines = session.config["stata_check_log_lines"]
4148

src/pytask_stata/parametrize.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ def pytask_parametrize_kwarg_to_marker(obj, kwargs):
88
"""Attach parametrized stata arguments to the function with a marker."""
99
if callable(obj):
1010
if "stata" in kwargs:
11-
mark.stata(*kwargs.pop("stata"))(obj)
11+
mark.stata(kwargs.pop("stata"))(obj)

src/pytask_stata/shared.py

+7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Shared functions and variables."""
22
import sys
33

4+
45
if sys.platform == "darwin":
56
STATA_COMMANDS = [
67
"Stata64MP",
@@ -54,3 +55,9 @@ def convert_task_id_to_name_of_log_file(id_):
5455
id_without_parent_directories = id_.rsplit("/")[-1]
5556
converted_id = id_without_parent_directories.replace(".", "_").replace("::", "_")
5657
return converted_id
58+
59+
60+
def get_node_from_dictionary(obj, key, fallback=0):
61+
if isinstance(obj, dict):
62+
obj = obj.get(key) or obj.get(fallback)
63+
return obj

tests/test_collect.py

+23-9
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
import pytest
55
from _pytask.mark import Mark
66
from _pytask.nodes import FilePathNode
7-
from pytask_stata.collect import _get_node_from_dictionary
87
from pytask_stata.collect import _merge_all_markers
98
from pytask_stata.collect import _prepare_cmd_options
109
from pytask_stata.collect import pytask_collect_task
1110
from pytask_stata.collect import pytask_collect_task_teardown
1211
from pytask_stata.collect import stata
12+
from pytask_stata.shared import get_node_from_dictionary
1313

1414

1515
class DummyClass:
@@ -65,26 +65,34 @@ def test_merge_all_markers(marks, expected):
6565
],
6666
)
6767
@pytest.mark.parametrize("stata_source_key", ["source", "do"])
68-
def test_prepare_cmd_options(args, stata_source_key):
68+
@pytest.mark.parametrize("platform", ["win32", "linux", "darwin"])
69+
def test_prepare_cmd_options(args, stata_source_key, platform):
6970
session = DummyClass()
70-
session.config = {"stata": "stata", "stata_source_key": stata_source_key}
71+
session.config = {
72+
"stata": "stata",
73+
"stata_source_key": stata_source_key,
74+
"platform": platform,
75+
}
7176

7277
node = DummyClass()
73-
node.value = Path("script.do")
78+
node.path = Path("script.do")
7479
task = DummyClass()
7580
task.depends_on = {stata_source_key: node}
7681
task.name = "task"
7782

7883
result = _prepare_cmd_options(session, task, args)
7984

80-
assert result == [
85+
expected = [
8186
"stata",
8287
"-e",
8388
"do",
8489
"script.do",
8590
*args,
86-
"-task",
8791
]
92+
if platform == "win32":
93+
expected.append("-task")
94+
95+
assert result == expected
8896

8997

9098
@pytest.mark.unit
@@ -115,9 +123,14 @@ def test_pytask_collect_task(name, expected):
115123
(["input.dta", "script.do"], ["any_out.dta"], pytest.raises(ValueError)),
116124
],
117125
)
118-
def test_pytask_collect_task_teardown(depends_on, produces, expectation):
126+
@pytest.mark.parametrize("platform", ["win32", "darwin", "linux"])
127+
def test_pytask_collect_task_teardown(depends_on, produces, platform, expectation):
119128
session = DummyClass()
120-
session.config = {"stata": "stata", "stata_source_key": "source"}
129+
session.config = {
130+
"stata": "stata",
131+
"stata_source_key": "source",
132+
"platform": platform,
133+
}
121134

122135
task = DummyClass()
123136
task.depends_on = {
@@ -126,6 +139,7 @@ def test_pytask_collect_task_teardown(depends_on, produces, expectation):
126139
task.produces = {i: FilePathNode.from_path(Path(n)) for i, n in enumerate(produces)}
127140
task.function = task_dummy
128141
task.name = "task_dummy"
142+
task.path = Path()
129143

130144
markers = [Mark("stata", (), {})]
131145
task.markers = markers
@@ -146,5 +160,5 @@ def test_pytask_collect_task_teardown(depends_on, produces, expectation):
146160
],
147161
)
148162
def test_get_node_from_dictionary(obj, key, expected):
149-
result = _get_node_from_dictionary(obj, key)
163+
result = get_node_from_dictionary(obj, key)
150164
assert result == expected

0 commit comments

Comments
 (0)