Skip to content

Commit

Permalink
download fix (#10)
Browse files Browse the repository at this point in the history
* reworked the downloader

* resolver fixes

* ruff fixes

* Final downloader fixes
  • Loading branch information
JessicaTegner authored Feb 22, 2025
1 parent 9440232 commit c6ce880
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 109 deletions.
82 changes: 28 additions & 54 deletions pytinytex/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import sys
import subprocess
import os
import subprocess
import platform
from pathlib import Path

from .tinytex_download import download_tinytex # noqa
from .tinytex_download import download_tinytex, DEFAULT_TARGET_FOLDER # noqa

# Global cache
__tinytex_path = getattr(os.environ, "PYTINYTEX_TINYTEX", None)
__tinytex_path = None

def update(package="-all"):
path = get_tinytex_path()
Expand All @@ -19,14 +18,16 @@ def update(package="-all"):
return False


def get_tinytex_path(base="."):
global __tinytex_path
if __tinytex_path is not None:
def get_tinytex_path(base=None):
if __tinytex_path:
return __tinytex_path
path_to_resolve = DEFAULT_TARGET_FOLDER
if base:
path_to_resolve = base
if os.environ.get("PYTINYTEX_TINYTEX", None):
path_to_resolve = os.environ["PYTINYTEX_TINYTEX"]

ensure_tinytex_installed(base)
if __tinytex_path is None:
raise RuntimeError("TinyTeX doesn't seem to be installed. You can install TinyTeX with pytinytex.download_tinytex().")
ensure_tinytex_installed(path_to_resolve)
return __tinytex_path

def get_pdf_latex_engine():
Expand All @@ -36,60 +37,33 @@ def get_pdf_latex_engine():
return os.path.join(get_tinytex_path(), "pdflatex")


def ensure_tinytex_installed(path="."):
def ensure_tinytex_installed(path=None):
global __tinytex_path
error_path = None
try:
if __tinytex_path is not None:
error_path = __tinytex_path
__tinytex_path = _resolve_path(__tinytex_path)
else:
error_path = path
__tinytex_path = _resolve_path(path)
return True
except RuntimeError:
__tinytex_path = None
raise RuntimeError("Unable to resolve TinyTeX path. Got as far as {}".format(error_path))
return False
if not path:
path = __tinytex_path
__tinytex_path = _resolve_path(path)
return True

def _resolve_path(path="."):
while True:
def _resolve_path(path):
try:
if _check_file(path, "tlmgr"):
return str(Path(path).resolve())
new_path = ""
list_dir = os.listdir(path)
if "bin" in list_dir:
new_path = _jump_folder(os.path.join(path, "bin"))
elif "tinytex" in list_dir:
new_path = _jump_folder(os.path.join(path, "tinytex"))
elif ".tinytex" in list_dir:
new_path = _jump_folder(os.path.join(path, ".tinytex"))
else:
new_path = _jump_folder(path)
if new_path is not None:
path = new_path

def _jump_folder(path):
dir_index = os.listdir(path)
if len(dir_index) == 1:
if os.path.isdir(os.path.join(path, dir_index[0])):
return _resolve_path(os.path.join(path, dir_index[0]))
else:
for directory in dir_index:
if os.path.isdir(os.path.join(path, directory)):
try:
return _resolve_path(os.path.join(path, directory))
except RuntimeError:
pass
raise RuntimeError("Unable to resolve TinyTeX path.")
return path
# if there is a bin folder, go into it
if os.path.isdir(os.path.join(path, "bin")):
return _resolve_path(os.path.join(path, "bin"))
# if there is only 1 folder in the path, go into it
if len(os.listdir(path)) == 1:
return _resolve_path(os.path.join(path, os.listdir(path)[0]))
except FileNotFoundError:
pass
raise RuntimeError(f"Unable to resolve TinyTeX path.\nTried {path}.\nYou can install TinyTeX using pytinytex.download_tinytex()")

def _check_file(dir, prefix):
try:
for s in os.listdir(dir):
if os.path.splitext(s)[0] == prefix and os.path.isfile(os.path.join(dir, s)):
return True
except FileNotFoundError:
raise RuntimeError("Unable to resolve path.")
return False

def _get_file(dir, prefix):
Expand Down
89 changes: 54 additions & 35 deletions pytinytex/tinytex_download.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import sys
import os

import re
import platform
import shutil
import urllib
import zipfile
import tarfile
from pathlib import Path
import tempfile

try:
from urllib.request import urlopen
except ImportError:
from urllib import urlopen

DEFAULT_TARGET_FOLDER = Path.home() / ".pytinytex"

def download_tinytex(version="latest", variation=1, target_folder=".", download_folder=None):
def download_tinytex(version="latest", variation=1, target_folder=DEFAULT_TARGET_FOLDER, download_folder=None):
if variation not in [0, 1, 2]:
raise RuntimeError("Invalid TinyTeX variation {}. Valid variations are 0, 1, 2.".format(variation))
if re.match(r"\d{4}\.\d{2}", version) or version == "latest":
if version != "latest":
version = "v" + version
else:
raise RuntimeError("Invalid TinyTeX version {}. TinyTeX version has to be in the format 'latest' for the latest available version, or year.month, for example: '2024.12', '2024.09' for a specific version.".format(version))
variation = str(variation)
pf = sys.platform
if pf.startswith("linux"):
Expand All @@ -26,44 +36,54 @@ def download_tinytex(version="latest", variation=1, target_folder=".", download_
raise RuntimeError("Can't handle your platform (only Linux, Mac OS X, Windows).")
url = tinytex_urls[pf]
filename = url.split("/")[-1]
if download_folder is not None:
if download_folder.endswith('/'):
download_folder = download_folder[:-1]
if download_folder is None:
download_folder = "."
filename = os.path.join(os.path.expanduser(download_folder), filename)
if os.path.isfile(filename):
print("* Using already downloaded file %s" % (filename))
if download_folder:
download_folder = Path(download_folder)
else:
download_folder = Path(".")
if target_folder:
target_folder = Path(target_folder)
# make sure all the folders exist
download_folder.mkdir(parents=True, exist_ok=True)
target_folder.mkdir(parents=True, exist_ok=True)
filename = download_folder / filename
if filename.exists():
print("* Using already downloaded file %s" % (str(filename)))
else:
print("* Downloading TinyTeX from %s ..." % url)
response = urlopen(url)
with open(filename, 'wb') as out_file:
shutil.copyfileobj(response, out_file)
print("* Downloaded TinyTeX, saved in %s ..." % filename)

print("Extracting %s to %s..." % (filename, target_folder))
extracted_dir_name = "TinyTeX"
if filename.endswith(".zip"):
zf = zipfile.ZipFile(filename)
zf.extractall(target_folder)
zf.close()
elif filename.endswith(".tgz"):
tf = tarfile.open(filename, "r:gz")
tf.extractall(target_folder)
tf.close()
elif filename.endswith(".tar.gz"):
tf = tarfile.open(filename, "r:gz")
tf.extractall(target_folder)
tf.close()
extracted_dir_name = ".TinyTeX"
else:
raise RuntimeError("File {0} not supported".format(filename))
tinytex_extracted = os.path.join(target_folder, extracted_dir_name)
for file_name in os.listdir(tinytex_extracted):
shutil.move(os.path.join(tinytex_extracted, file_name), target_folder)
shutil.rmtree(tinytex_extracted)
print("Adding TinyTeX to path")
sys.path.insert(0, os.path.join(target_folder, "bin"))
print("Extracting %s to a temporary folder..." % filename)
with tempfile.TemporaryDirectory() as tmpdirname:
tmpdirname = Path(tmpdirname)
extracted_dir_name = "TinyTeX" # for Windows and MacOS
if filename.suffix == ".zip":
zf = zipfile.ZipFile(filename)
zf.extractall(tmpdirname)
zf.close()
elif filename.suffix == ".tgz":
tf = tarfile.open(filename, "r:gz")
tf.extractall(tmpdirname)
tf.close()
elif filename.suffix == ".gz":
tf = tarfile.open(filename, "r:gz")
tf.extractall(tmpdirname)
tf.close()
extracted_dir_name = ".TinyTeX" # for linux only
else:
raise RuntimeError("File {0} not supported".format(filename))
tinytex_extracted = tmpdirname / extracted_dir_name
# copy the extracted folder to the target folder, overwriting if necessary
print("Copying TinyTeX to %s..." % target_folder)
shutil.copytree(tinytex_extracted, target_folder, dirs_exist_ok=True)
# go into target_folder/bin, and as long as we keep having 1 and only 1 subfolder, go into that, and add it to path
folder_to_add_to_path = target_folder / "bin"
while len(list(folder_to_add_to_path.glob("*"))) == 1 and folder_to_add_to_path.is_dir():
folder_to_add_to_path = list(folder_to_add_to_path.glob("*"))[0]
print(f"Adding TinyTeX to path ({str(folder_to_add_to_path)})...")
sys.path.append(str(folder_to_add_to_path))
print("Done")

def _get_tinytex_urls(version, variation):
Expand All @@ -75,8 +95,7 @@ def _get_tinytex_urls(version, variation):
version_url_frags = response.url.split("/")
version = version_url_frags[-1]
except urllib.error.HTTPError:
raise RuntimeError("Invalid TinyTeX version {}.".format(version))
return
raise RuntimeError("Can't find TinyTeX version %s" % version)
# read the HTML content
response = urlopen("https://github.com/rstudio/tinytex-releases/releases/expanded_assets/"+version)
content = response.read()
Expand Down
29 changes: 26 additions & 3 deletions tests/test_tinytex_download.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
import os
import shutil
import pytest

from .utils import download_tinytex # noqa
import pytinytex

def test_successful_download(download_tinytex): # noqa
def test_successful_download(): # noqa
pytinytex.download_tinytex(variation=0, target_folder="tests/tinytex_distribution", download_folder="tests")
assert os.path.isdir(os.path.join("tests", "tinytex_distribution"))
assert os.path.isdir(os.path.join("tests", "tinytex_distribution", "bin"))
shutil.rmtree(os.path.join("tests", "tinytex_distribution"))
for item in os.listdir("tests"):
if item.endswith(".zip") or item.endswith(".tar.gz") or item.endswith(".tgz"):
os.remove(os.path.join("tests", item))

def test_bin_is_in_distribution(download_tinytex): # noqa
def test_successful_download_specific_version():
pytinytex.download_tinytex(variation=0, version="2024.12", target_folder="tests/tinytex_distribution", download_folder="tests")
assert os.path.isdir(os.path.join("tests", "tinytex_distribution"))
assert os.path.isdir(os.path.join("tests", "tinytex_distribution", "bin"))
shutil.rmtree(os.path.join("tests", "tinytex_distribution"))
# delete any files in the test dir that ends with zip, gz or tgz
for item in os.listdir("tests"):
if item.endswith(".zip") or item.endswith(".tar.gz") or item.endswith(".tgz"):
os.remove(os.path.join("tests", item))

def test_failing_download_invalid_variation():
with pytest.raises(RuntimeError, match="Invalid TinyTeX variation 999."):
pytinytex.download_tinytex(variation=999)

def test_failing_download_invalid_version():
with pytest.raises(RuntimeError, match="Invalid TinyTeX version invalid."):
pytinytex.download_tinytex(version="invalid")
24 changes: 9 additions & 15 deletions tests/test_tinytex_path_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,26 @@
import pytest

import pytinytex
from .utils import download_tinytex # noqa

def test_empty_cache(download_tinytex): # noqa
assert pytinytex.__tinytex_path is None
from .utils import download_tinytex, TINYTEX_DISTRIBUTION # noqa

def test_failing_resolver(download_tinytex): # noqa
assert pytinytex.__tinytex_path is None
with pytest.raises(RuntimeError):
pytinytex._resolve_path("failing")
assert pytinytex.__tinytex_path is None
with pytest.raises(RuntimeError):
pytinytex.ensure_tinytex_installed("failing")
assert pytinytex.__tinytex_path is None

def test_successful_resolver(download_tinytex): # noqa
assert pytinytex.__tinytex_path is None
pytinytex.ensure_tinytex_installed("tests")
pytinytex.ensure_tinytex_installed(TINYTEX_DISTRIBUTION)
assert isinstance(pytinytex.__tinytex_path, str)
assert os.path.isdir(pytinytex.__tinytex_path)

def test_get_tinytex_path(download_tinytex): # noqa
pytinytex.ensure_tinytex_installed("tests")
assert isinstance(pytinytex.get_tinytex_path(), str)
assert pytinytex.__tinytex_path == pytinytex.get_tinytex_path("tests")
# actually resolve the path
pytinytex.ensure_tinytex_installed(TINYTEX_DISTRIBUTION)
assert pytinytex.__tinytex_path == pytinytex.get_tinytex_path(TINYTEX_DISTRIBUTION)

def get_pdf_latex_engine(download_tinytex): # noqa
pytinytex.ensure_tinytex_installed("tests")
@pytest.mark.parametrize("download_tinytex", [1], indirect=True)
def test_get_pdf_latex_engine(download_tinytex): # noqa
pytinytex.ensure_tinytex_installed(TINYTEX_DISTRIBUTION)
assert isinstance(pytinytex.get_pdf_latex_engine(), str)
assert os.path.isfile(pytinytex.get_pdf_latex_engine())
assert os.path.isfile(pytinytex.get_pdf_latex_engine())
10 changes: 8 additions & 2 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@

import pytinytex

TINYTEX_DISTRIBUTION = os.path.join("tests", "tinytex_distribution")

@pytest.fixture(scope="module")
def download_tinytex():
yield pytinytex.download_tinytex(variation=0, target_folder=os.path.join("tests", "tinytex_distribution"), download_folder="tests")
def download_tinytex(request):
try:
variation = request.param
except AttributeError:
variation = 0
yield pytinytex.download_tinytex(variation=variation, target_folder=TINYTEX_DISTRIBUTION, download_folder="tests")
cleanup()


Expand Down

0 comments on commit c6ce880

Please # to comment.