Skip to content

Commit efc0677

Browse files
init: Simply import grass, try harder if it fails (#6393)
Previously, the path with the package was required to exist (both the hardcoded etc/python under GISBASE and FHS variable one). Now, the code first attempts the import and only when the import fails, it tries to work out the path. The simple import should work when FHS is actually used and the grass package is where Python looks for packages (so no GRASS_PYDIR is needed there). It will also work when a user sets up PYTHONPATH manually expecting it will work the same as when setting it before running Python and importing the grass package there (both PYTHONPATH and GRASS_PYDIR will work, but PYTHONPATH will user the Python native mechanics to make the import work, while GRASS_PYDIR needs to use sys.path.append). If GRASS_PYDIR does not work (either the env variable or the one from the build), the code will attempt to find the GISBASE directory relative to the main executable and expects a non-FHS layout (which is reasonable since import should work without any setup with FHS). Prepend, not append to sys.path. Remove sys.path manipulations from the rest of grass.py file because the smarter code to do that is already executed by this point. * Try to import more specific grass.script, not just general grass to avoid having the import work for grass.py (issue on Windows where .py is not removed). If there is something which was imported as grass (grass.py would be the case), but does not have script, we need to convince Python to repeat the import of grass. We do that by removing it from sys.modules cache. This gives us a fresh start for next import with new path at the beginning of sys.path. Use a more specific exception type (available since Python 3.6). * Apply suggestions from code review --------- Co-authored-by: Anna Petrasova <[email protected]>
1 parent a5a43ae commit efc0677

File tree

1 file changed

+79
-19
lines changed

1 file changed

+79
-19
lines changed

lib/init/grass.py

Lines changed: 79 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -660,8 +660,6 @@ def create_location(gisdbase, location, geostring) -> None:
660660
:param location: name of new Location
661661
:param geostring: path to a georeferenced file or EPSG code
662662
"""
663-
if gpath("etc", "python") not in sys.path:
664-
sys.path.append(gpath("etc", "python"))
665663
from grass.script import core as gcore # pylint: disable=E0611
666664

667665
try:
@@ -1434,8 +1432,6 @@ def start_gui(grass_gui: Literal["wxpython"]):
14341432

14351433
def close_gui() -> None:
14361434
"""Close GUI if running"""
1437-
if gpath("etc", "python") not in sys.path:
1438-
sys.path.append(gpath("etc", "python"))
14391435
from grass.script import core as gcore # pylint: disable=E0611
14401436

14411437
env = gcore.gisenv()
@@ -2081,25 +2077,89 @@ def validate_cmdline(params: Parameters) -> None:
20812077

20822078

20832079
def find_grass_python_package() -> None:
2084-
"""Find path to grass package and add it to path"""
2080+
"""Find path to the grass package and add it to path if needed"""
2081+
# Whether or not the pre-set path exists, the environment may be just
2082+
# set up right already. Let's try a basic import first.
2083+
try:
2084+
import grass.script as unused_gs # noqa: F401, ICN001
20852085

2086-
# The "@...@" variables are being substituted during build process
2086+
# The import works without any setup, so there is nothing more to do.
2087+
return
2088+
except ModuleNotFoundError:
2089+
# If the grass package is not on path, we need to add it to path.
2090+
pass
20872091

2088-
if "GRASS_PYDIR" in os.environ and len(os.getenv("GRASS_PYDIR")) > 0:
2089-
GRASS_PYDIR = os.path.normpath(os.environ["GRASS_PYDIR"])
2090-
else:
2091-
GRASS_PYDIR = os.path.normpath(r"@GRASS_PYDIR@")
2092+
# If we happened to import something else, like our startup script called grass.py,
2093+
# we need to first remove it from the import cache (this does not truly un-import,
2094+
# but it should be sufficient for our startup script).
2095+
if "grass" in sys.modules:
2096+
del sys.modules["grass"]
20922097

2093-
if os.path.exists(GRASS_PYDIR):
2094-
sys.path.append(GRASS_PYDIR)
2095-
# now we can import stuff from grass package
2098+
# Try to find the package.
2099+
path, exists = find_path_to_grass_python_package()
2100+
if exists:
2101+
sys.path.insert(0, path)
2102+
try:
2103+
# We don't make assumptions about what should be in the directory
2104+
# and we simply try the actual import.
2105+
import grass.script as unused_gs_2nd_attempt # noqa: F401, ICN001
2106+
2107+
# If the import worked, we did our part.
2108+
return
2109+
except ModuleNotFoundError as error:
2110+
# Existing path provided, but there is some issue with the import.
2111+
# It may be a wrong path or issue in the package itself.
2112+
# These strings are not translatable because we can't load translations.
2113+
msg = (
2114+
f"The grass Python package cannot be imported from {path}. "
2115+
"Try setting PYTHONPATH or GRASS_PYDIR to where the grass package is."
2116+
)
2117+
raise RuntimeError(msg) from error
2118+
# The path provided by the build or by the user does not exist.
2119+
msg = (
2120+
f"{path} with the grass Python package does not exist. "
2121+
"Is the installation of GRASS complete?"
2122+
)
2123+
raise RuntimeError(msg)
2124+
2125+
2126+
def find_path_to_grass_python_package() -> tuple[str, bool]:
2127+
"""Returns the most likely path to the grass package.
2128+
2129+
It prefers the directory provided by the user in an environmental variable.
2130+
Otherwise, it uses the build time variable.
2131+
It falls back to a heuristic based on where this file is located.
2132+
If that fails, it returns the actual set path
2133+
(and returns False for existence).
2134+
2135+
:return: tuple with path as a string and boolean for existence
2136+
"""
2137+
env_variable = os.environ.get("GRASS_PYDIR", None)
2138+
if env_variable:
2139+
path_from_variable = os.path.normpath(env_variable)
20962140
else:
2097-
# Not translatable because we don't have translations loaded.
2098-
msg = (
2099-
"The grass Python package is missing. "
2100-
"Is the installation of GRASS complete?"
2101-
)
2102-
raise RuntimeError(msg)
2141+
# The "@...@" variables are being substituted during build process
2142+
path_from_variable = os.path.normpath(r"@GRASS_PYDIR@")
2143+
if os.path.exists(path_from_variable):
2144+
return path_from_variable, True
2145+
2146+
base = Path(__file__).parent.parent / "lib"
2147+
path_from_context = base / "grass" / "etc" / "python"
2148+
if os.path.exists(path_from_context):
2149+
return str(path_from_context), True
2150+
2151+
major = "@GRASS_VERSION_MAJOR@"
2152+
minor = "@GRASS_VERSION_MINOR@"
2153+
# Try a run-together version number for the directory (long-used standard).
2154+
path_from_context = base / f"grass{major}{minor}" / "etc" / "python"
2155+
if os.path.exists(path_from_context):
2156+
return str(path_from_context), True
2157+
# Try a dotted version number (more common standard).
2158+
path_from_context = base / f"grass{major}.{minor}" / "etc" / "python"
2159+
if os.path.exists(path_from_context):
2160+
return str(path_from_context), True
2161+
2162+
return path_from_variable, False
21032163

21042164

21052165
def main() -> None:

0 commit comments

Comments
 (0)