|
74 | 74 | from fnmatch import fnmatch |
75 | 75 | from importlib.util import find_spec |
76 | 76 | from pathlib import Path |
77 | | -from typing import ClassVar |
| 77 | +from typing import ClassVar, TextIO |
78 | 78 |
|
79 | 79 | import numpy as np |
80 | 80 | from more_itertools import flatten, peekable |
81 | 81 | from numpy.lib.recfunctions import structured_to_unstructured |
82 | 82 |
|
83 | | -from parsnip._errors import ParseWarning |
| 83 | +from parsnip._errors import ParseWarning, _is_potentially_valid_path |
84 | 84 | from parsnip.patterns import ( |
85 | 85 | _accumulate_nonsimple_data, |
86 | 86 | _box_from_lengths_and_angles, |
@@ -111,7 +111,7 @@ class CifFile: |
111 | 111 | >>> from parsnip import CifFile |
112 | 112 | >>> cif = CifFile("example_file.cif") |
113 | 113 | >>> print(cif) |
114 | | - CifFile(fn=example_file.cif) : 12 data entries, 2 data loops |
| 114 | + CifFile(file=example_file.cif) : 12 data entries, 2 data loops |
115 | 115 |
|
116 | 116 | Data entries are accessible via the :attr:`~.pairs` and :attr:`~.loops` attributes: |
117 | 117 |
|
@@ -141,21 +141,38 @@ class CifFile: |
141 | 141 | Default value = ``False`` |
142 | 142 | """ |
143 | 143 |
|
144 | | - def __init__(self, fn: str | Path, cast_values: bool = False): |
145 | | - """Create a CifFile object from a filename. |
| 144 | + def __init__( |
| 145 | + self, file: str | Path | TextIO | Iterable[str], cast_values: bool = False |
| 146 | + ): |
| 147 | + """Create a CifFile object from a filename, file object, or iterator over `str`. |
146 | 148 |
|
147 | 149 | On construction, the entire file is parsed into key-value pairs and data loops. |
148 | 150 | Comment lines are ignored. |
149 | 151 |
|
150 | 152 | """ |
151 | | - self._fn = fn |
| 153 | + self._fn = file |
152 | 154 | self._pairs = {} |
153 | 155 | self._loops = [] |
154 | 156 |
|
155 | 157 | self._cpat = {k: re.compile(pattern) for (k, pattern) in self.PATTERNS.items()} |
156 | 158 | self._cast_values = cast_values |
157 | 159 |
|
158 | | - with open(fn) as file: |
| 160 | + if (isinstance(file, str) and _is_potentially_valid_path(file)) or isinstance( |
| 161 | + file, Path |
| 162 | + ): |
| 163 | + with open(file) as file: |
| 164 | + self._parse(peekable(file)) |
| 165 | + # We expect a TextIO | IOBase, but allow users to pass any Iterable[string_like] |
| 166 | + # This includes a str that does not point to a file! |
| 167 | + elif isinstance(file, str): |
| 168 | + msg = ( |
| 169 | + "\nFile input was parsed as a raw CIF data block. " |
| 170 | + "If you intended to read the input string as a file path, please " |
| 171 | + "ensure it is validly formatted." |
| 172 | + ) |
| 173 | + warnings.warn(msg, RuntimeWarning, stacklevel=2) |
| 174 | + self._parse(peekable(file.splitlines(True))) |
| 175 | + else: |
159 | 176 | self._parse(peekable(file)) |
160 | 177 |
|
161 | 178 | _SYMPY_AVAILABLE = find_spec("sympy") is not None |
@@ -919,7 +936,7 @@ def _parse(self, data_iter: Iterable): |
919 | 936 | def __repr__(self): |
920 | 937 | n_pairs = len(self.pairs) |
921 | 938 | n_tabs = len(self.loops) |
922 | | - return f"CifFile(fn={self._fn}) : {n_pairs} data entries, {n_tabs} data loops" |
| 939 | + return f"CifFile(file={self._fn}) : {n_pairs} data entries, {n_tabs} data loops" |
923 | 940 |
|
924 | 941 | PATTERNS: ClassVar = { |
925 | 942 | "key_value_general": r"^(_[\w\.\-/\[\d\]]+)\s+([^#]+)", |
|
0 commit comments