Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: common.openlane.Path inheriting from pathlib.Path #669

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions openlane/common/toolbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
from .metrics import aggregate_metrics
from .generic_dict import GenericImmutableDict, is_string
from ..state import DesignFormat
from ..common import Filter
from ..common import Filter, AnyPath
from ..logging import debug, warn, err


Expand All @@ -56,10 +56,10 @@ class Toolbox(object):
between steps.
"""

def __init__(self, tmp_dir: str) -> None:
def __init__(self, tmp_dir: AnyPath) -> None:
# Only create before use, otherwise users will end up with
# "openlane_run/tmp" created in their PWD because of the global toolbox
self.tmp_dir = tmp_dir
self.tmp_dir = Path(tmp_dir)

self.remove_cells_from_lib = lru_cache(16, True)(self.remove_cells_from_lib) # type: ignore
self.create_blackbox_model = lru_cache(16, True)(self.create_blackbox_model) # type: ignore
Expand Down Expand Up @@ -100,7 +100,7 @@ def filter_views(

for key in Filter(views_by_corner).get_matching_wildcards(timing_corner):
value = views_by_corner[key]
if is_string(value):
if is_string(value) or isinstance(value, os.PathLike):
result += [value] # type: ignore
else:
result += list(value) # type: ignore
Expand Down Expand Up @@ -180,7 +180,7 @@ def get_macro_views(
elif views is not None:
result += [Path(views)]

return [element for element in result if str(element) != Path._dummy_path]
return result

def get_macro_views_by_priority(
self,
Expand Down
41 changes: 8 additions & 33 deletions openlane/common/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@
# limitations under the License.
import os
import sys
import pathlib
import tempfile
from math import isfinite
from decimal import Decimal
from collections import UserString
from typing import Any, Union, ClassVar, Tuple, Optional
from typing import Any, Union, Tuple, Optional


def is_string(obj: Any) -> bool:
Expand All @@ -35,35 +36,16 @@ def is_real_number(obj: Any) -> bool:
return is_number(obj) and isfinite(obj)


class Path(UserString, os.PathLike):
class Path(pathlib.Path):
"""
A Path type for OpenLane configuration variables.

Basically just a string.
"""

# This path will pass the validate() call, but will
# fail to open. It should be used for deprecated variable
# translation only.
_dummy_path: ClassVar[str] = "__openlane_dummy_path"

def __fspath__(self) -> str:
return str(self)

def __repr__(self) -> str:
return f"{self.__class__.__qualname__}('{self}')"

def exists(self) -> bool:
"""
A convenience method calling :meth:`os.path.exists`
"""
return os.path.exists(self)

def validate(self, message_on_err: str = ""):
"""
Raises an error if the path does not exist.
Raises an error if the path does not exist and it is not a dummy path.
"""
if not self.exists() and not self == Path._dummy_path:
if not self.exists():
raise ValueError(f"{message_on_err}: '{self}' does not exist")

def startswith(
Expand All @@ -72,22 +54,15 @@ def startswith(
start: Optional[int] = 0,
end: Optional[int] = sys.maxsize,
) -> bool:
if isinstance(prefix, UserString) or isinstance(prefix, os.PathLike):
prefix = str(prefix)
return super().startswith(prefix, start, end)
raise AttributeError("Path.startswith has been removed.")

def rel_if_child(
self,
start: Union[str, os.PathLike] = os.getcwd(),
*,
relative_prefix: str = "",
) -> "Path":
my_abspath = os.path.abspath(self)
start_abspath = os.path.abspath(start)
if my_abspath.startswith(start_abspath):
return Path(relative_prefix + os.path.relpath(self, start_abspath))
else:
return Path(my_abspath)
raise AttributeError("Path.rel_if_child has been removed.")


AnyPath = Union[str, os.PathLike]
Expand All @@ -98,7 +73,7 @@ class ScopedFile(Path):
Creates a temporary file that remains valid while this variable is in scope,
and is deleted upon deconstruction.

The object itself is a string pointing to that file path.
The object itself is a pathlib.Path pointing to that file path.

:param contents: The contents of the temporary file to create.
"""
Expand Down
32 changes: 18 additions & 14 deletions openlane/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
TclUtils,
AnyPath,
is_string,
Path,
)

AnyConfig = Union[AnyPath, Mapping[str, Any]]
Expand Down Expand Up @@ -346,7 +347,7 @@ def get_meta(
"""
default_meta_version = 2

if is_string(config_in):
if is_string(config_in) or isinstance(config_in, os.PathLike):
config_in = str(config_in)
validated_type = _validate_config_file(config_in)
if validated_type == "tcl":
Expand Down Expand Up @@ -379,7 +380,7 @@ def interactive(
DESIGN_NAME: str,
PDK: str,
STD_CELL_LIBRARY: Optional[str] = None,
PDK_ROOT: Optional[str] = None,
PDK_ROOT: Optional[AnyPath] = None,
**kwargs,
) -> "Config":
"""
Expand Down Expand Up @@ -407,7 +408,9 @@ def interactive(
Useful examples are CLOCK_PORT, CLOCK_PERIOD, et cetera, which while
not bound to a specific :class:`Step`, affects most Steps' behavior.
"""
PDK_ROOT = Self.__resolve_pdk_root(PDK_ROOT)
PDK_ROOT = Self.__resolve_pdk_root(
Path(PDK_ROOT) if PDK_ROOT is not None else None
)

raw, _, _ = Self.__get_pdk_config(
PDK,
Expand Down Expand Up @@ -449,8 +452,8 @@ def load(
flow_config_vars: Sequence[Variable],
*,
config_override_strings: Optional[Sequence[str]] = None,
pdk_root: Optional[AnyPath] = None,
pdk: Optional[str] = None,
pdk_root: Optional[str] = None,
scl: Optional[str] = None,
design_dir: Optional[str] = None,
_load_pdk_configs: bool = True,
Expand Down Expand Up @@ -492,9 +495,10 @@ def load(

:returns: A tuple containing a Config object and the design directory.
"""
pdk_root = Path(pdk_root) if pdk_root is not None else pdk_root
if isinstance(config_in, Mapping):
config_in = [config_in]
elif is_string(config_in):
elif is_string(config_in) or isinstance(config_in, os.PathLike):
config_in = [str(config_in)]

assert not isinstance(config_in, str)
Expand Down Expand Up @@ -632,7 +636,7 @@ def __load_dict(
flow_config_vars: Sequence[Variable],
*,
meta: Meta,
pdk_root: Optional[str] = None,
pdk_root: Optional[Path] = None,
pdk: Optional[str] = None,
scl: Optional[str] = None,
full_pdk_warnings: bool = False,
Expand Down Expand Up @@ -731,7 +735,7 @@ def __mapping_from_tcl(
config: AnyPath,
design_dir: str,
*,
pdk_root: Optional[str] = None,
pdk_root: Optional[Path] = None,
pdk: Optional[str] = None,
scl: Optional[str] = None,
) -> Mapping[str, Any]:
Expand Down Expand Up @@ -784,28 +788,28 @@ def __mapping_from_tcl(
@classmethod
def __resolve_pdk_root(
Self,
pdk_root: Optional[str],
) -> str:
pdk_root: Optional[Path],
) -> Path:
if pdk_root is None:
try:
import volare

pdk_root = volare.get_volare_home(pdk_root)
pdk_root = Path(volare.get_volare_home(pdk_root))
except ImportError:
raise ValueError(
"The pdk_root argument is required as Volare is not installed."
)

return os.path.abspath(pdk_root)
return pdk_root.absolute()

@staticmethod
@lru_cache(1, True)
def __get_pdk_raw(
pdk_root: str, pdk: str, scl: Optional[str]
pdk_root: Path, pdk: str, scl: Optional[str]
) -> Tuple[GenericImmutableDict[str, Any], str, str]:
pdk_config: GenericDict[str, Any] = GenericDict(
{
SpecialKeys.pdk_root: pdk_root,
SpecialKeys.pdk_root: str(pdk_root),
SpecialKeys.pdk: pdk,
}
)
Expand Down Expand Up @@ -852,7 +856,7 @@ def __get_pdk_raw(
def __get_pdk_config(
pdk: str,
scl: Optional[str],
pdk_root: str,
pdk_root: Optional[Path],
flow_pdk_vars: Optional[List[Variable]] = None,
full_pdk_warnings: Optional[bool] = False,
) -> Tuple[GenericDict[str, Any], str, str]:
Expand Down
4 changes: 2 additions & 2 deletions openlane/config/preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from types import SimpleNamespace
from typing import Any, Dict, List, Mapping, Sequence, Tuple, Union, Optional

from ..common import is_string
from ..common import is_string, Path

Keys = SimpleNamespace(
pdk_root="PDK_ROOT",
Expand Down Expand Up @@ -250,7 +250,7 @@ def process_string(
if target is None:
return None

if not is_string(target):
if not is_string(target) and not isinstance(target, Path):
if type(target) in [int, float, Decimal]:
raise TypeError(
f"Referenced variable {reference_variable} is a number and not a string: use expr::{match[0]} if you want to reference this number."
Expand Down
24 changes: 21 additions & 3 deletions openlane/config/variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,11 @@ def __str__(self) -> str:
@classmethod
def from_state(Self, state: State) -> "Macro":
kwargs = {}

for macro_field in fields(Self):
views = state.get(macro_field.name)
if views is None:
view = state.get(macro_field.name)
design_format = DesignFormat.factory.get(macro_field.name)
if view is None:
if macro_field.default_factory is not MISSING:
kwargs[macro_field.name] = macro_field.default_factory()
elif macro_field.default is not MISSING:
Expand All @@ -198,10 +200,26 @@ def from_state(Self, state: State) -> "Macro":
)
continue
var_name = f"{Self.__name__}.{macro_field.name}"
views: Union[Dict[str, List[Path]], List[Path]]
if design_format.multiple:
assert isinstance(
view, dict
), "expected multiple views, but did not find a dict"
views = {}
for key, value in view.items():
assert isinstance(
value, Path
), f"value for {macro_field.name}.{key} was not a singular path"
views[key] = [value]
else:
assert isinstance(
view, Path
), "expected single view, but did not find a Path"
views = [view]
_, final = Variable(var_name, macro_field.type, "").compile(
GenericDict({var_name: views}),
warning_list_ref=[],
permissive_typing=True,
permissive_typing=False,
)
kwargs[macro_field.name] = final

Expand Down
2 changes: 1 addition & 1 deletion openlane/steps/odb.py
Original file line number Diff line number Diff line change
Expand Up @@ -928,7 +928,7 @@ def get_command(self) -> List[str]:
command.append("--buffer-list")
command.append(self.get_buffer_list_file())
command.append("--out-dir")
command.append(self.step_dir)
command.append(str(self.step_dir))
return command


Expand Down
Loading
Loading