Skip to content

download fix #10

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

Merged
merged 4 commits into from
Feb 22, 2025
Merged
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
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
Loading