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

Create new conan.tools.files #8550

Merged
merged 26 commits into from Mar 1, 2021
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions conan/tools/files/__init__.py
@@ -0,0 +1 @@
from .files import load, save, mkdir, ftp_download, download, get
czoido marked this conversation as resolved.
Show resolved Hide resolved
150 changes: 150 additions & 0 deletions conan/tools/files/files.py
@@ -0,0 +1,150 @@
import errno
import os

from conans.errors import ConanException
from conans.util.files import decode_text, to_file_bytes
from conans.client.tools.files import unzip
from conans.client.downloaders.download import run_downloader


def load(conanfile, path, binary=False, encoding="auto"):
""" Loads a file content """
with open(path, 'rb') as handle:
tmp = handle.read()
return tmp if binary else decode_text(tmp, encoding)
czoido marked this conversation as resolved.
Show resolved Hide resolved


def save(conanfile, path, content, append=False):
if append:
mode = "ab"
try:
os.makedirs(os.path.dirname(path))
except Exception:
pass
else:
mode = "wb"
dir_path = os.path.dirname(path)
if not os.path.isdir(dir_path):
try:
os.makedirs(dir_path)
except OSError as error:
if error.errno not in (errno.EEXIST, errno.ENOENT):
raise OSError("The folder {} does not exist and could not be created ({})."
.format(dir_path, error.strerror))
except Exception:
raise

with open(path, mode) as handle:
handle.write(to_file_bytes(content, encoding="utf-8"))


def mkdir(conanfile, path):
"""Recursive mkdir, doesnt fail if already existing"""
if os.path.exists(path):
return
os.makedirs(path)


def get(conanfile, url, md5='', sha1='', sha256='', destination=".", filename="", keep_permissions=False,
pattern=None, verify=True, retry=None, retry_wait=None,
overwrite=False, auth=None, headers=None, strip_root=False):
""" high level downloader + unzipper + (optional hash checker) + delete temporary zip
"""
requester = conanfile._conan_requester
output = conanfile.output
if not filename: # deduce filename from the URL
url_base = url[0] if isinstance(url, (list, tuple)) else url
if "?" in url_base or "=" in url_base:
raise ConanException("Cannot deduce file name from the url: '{}'. Use 'filename' "
"parameter.".format(url_base))
filename = os.path.basename(url_base)

download(url, filename, out=output, requester=requester, verify=verify,
retry=retry, retry_wait=retry_wait, overwrite=overwrite, auth=auth, headers=headers,
md5=md5, sha1=sha1, sha256=sha256)
unzip(filename, destination=destination, keep_permissions=keep_permissions, pattern=pattern,
output=output, strip_root=strip_root)
os.unlink(filename)


def ftp_download(conanfile, ip, filename, login='', password=''):
import ftplib
try:
ftp = ftplib.FTP(ip)
ftp.login(login, password)
filepath, filename = os.path.split(filename)
if filepath:
ftp.cwd(filepath)
with open(filename, 'wb') as f:
ftp.retrbinary('RETR ' + filename, f.write)
except Exception as e:
try:
os.unlink(filename)
except OSError:
pass
raise ConanException("Error in FTP download from %s\n%s" % (ip, str(e)))
finally:
try:
ftp.quit()
except Exception:
pass


def download(conanfile, url, filename, verify=True, out=None, retry=None, retry_wait=None, overwrite=False,
auth=None, headers=None, requester=None, md5='', sha1='', sha256=''):
"""Retrieves a file from a given URL into a file with a given filename.
It uses certificates from a list of known verifiers for https downloads,
but this can be optionally disabled.

:param url: URL to download. It can be a list, which only the first one will be downloaded, and
the follow URLs will be used as mirror in case of download error.
:param filename: Name of the file to be created in the local storage
:param verify: When False, disables https certificate validation
:param out: An object with a write() method can be passed to get the output. stdout will use if
not specified
:param retry: Number of retries in case of failure. Default is overriden by general.retry in the
conan.conf file or an env variable CONAN_RETRY
:param retry_wait: Seconds to wait between download attempts. Default is overriden by
general.retry_wait in the conan.conf file or an env variable CONAN_RETRY_WAIT
:param overwrite: When True, Conan will overwrite the destination file if exists. Otherwise it
will raise an exception
:param auth: A tuple of user and password to use HTTPBasic authentication
:param headers: A dictionary with additional headers
:param requester: HTTP requests instance
:param md5: MD5 hash code to check the downloaded file
:param sha1: SHA-1 hash code to check the downloaded file
:param sha256: SHA-256 hash code to check the downloaded file
:return: None
"""
out = conanfile.output
requester = conanfile._conan_requester
config = conanfile._conan_config
czoido marked this conversation as resolved.
Show resolved Hide resolved

# It might be possible that users provide their own requester
retry = retry if retry is not None else config.retry
retry = retry if retry is not None else 1
retry_wait = retry_wait if retry_wait is not None else config.retry_wait
retry_wait = retry_wait if retry_wait is not None else 5

checksum = sha256 or sha1 or md5

def _download_file(file_url):
# The download cache is only used if a checksum is provided, otherwise, a normal download
run_downloader(requester=requester, output=out, verify=verify, config=config,
user_download=True, use_cache=bool(config and checksum), url=file_url,
file_path=filename, retry=retry, retry_wait=retry_wait, overwrite=overwrite,
auth=auth, headers=headers, md5=md5, sha1=sha1, sha256=sha256)
out.writeln("")

if not isinstance(url, (list, tuple)):
_download_file(url)
else: # We were provided several URLs to try
for url_it in url:
try:
_download_file(url_it)
break
except Exception as error:
message = "Could not download from the URL {}: {}.".format(url_it, str(error))
out.warn(message + " Trying another mirror.")
else:
raise ConanException("All downloads from ({}) URLs have failed.".format(len(url)))
3 changes: 2 additions & 1 deletion conans/client/conan_api.py
Expand Up @@ -203,7 +203,8 @@ def __init__(self, cache_folder, user_io, http_requester=None, runner=None, quie
self.generator_manager)
self.pyreq_loader = PyRequireLoader(self.proxy, self.range_resolver)
self.loader = ConanFileLoader(self.runner, self.out, self.python_requires,
self.generator_manager, self.pyreq_loader)
self.generator_manager, self.pyreq_loader, self.requester,
self.config)
czoido marked this conversation as resolved.
Show resolved Hide resolved

self.binaries_analyzer = GraphBinariesAnalyzer(self.cache, self.out, self.remote_manager)
self.graph_manager = GraphManager(self.out, self.cache, self.remote_manager, self.loader,
Expand Down
11 changes: 8 additions & 3 deletions conans/client/loader.py
Expand Up @@ -23,14 +23,17 @@

class ConanFileLoader(object):

def __init__(self, runner, output, python_requires, generator_manager=None, pyreq_loader=None):
def __init__(self, runner, output, python_requires, generator_manager=None, pyreq_loader=None,
requester=None, config=None):
self._runner = runner
self._generator_manager = generator_manager
self._output = output
self._pyreq_loader = pyreq_loader
self._python_requires = python_requires
sys.modules["conans"].python_requires = python_requires
self._cached_conanfile_classes = {}
self._requester = requester
self._config = config

def load_basic(self, conanfile_path, lock_python_requires=None, user=None, channel=None,
display=""):
Expand All @@ -45,7 +48,8 @@ def load_basic_module(self, conanfile_path, lock_python_requires=None, user=None
"""
cached = self._cached_conanfile_classes.get(conanfile_path)
if cached and cached[1] == lock_python_requires:
conanfile = cached[0](self._output, self._runner, display, user, channel)
conanfile = cached[0](self._output, self._runner, display, user, channel,
self._requester, self._config)
if hasattr(conanfile, "init") and callable(conanfile.init):
with conanfile_exception_formatter(str(conanfile), "init"):
conanfile.init()
Expand Down Expand Up @@ -82,7 +86,8 @@ def load_basic_module(self, conanfile_path, lock_python_requires=None, user=None

self._cached_conanfile_classes[conanfile_path] = (conanfile, lock_python_requires,
module)
result = conanfile(self._output, self._runner, display, user, channel)
result = conanfile(self._output, self._runner, display, user, channel, self._requester,
self._config)
if hasattr(result, "init") and callable(result.init):
with conanfile_exception_formatter(str(result), "init"):
result.init()
Expand Down
5 changes: 4 additions & 1 deletion conans/model/conan_file.py
Expand Up @@ -132,7 +132,8 @@ class ConanFile(object):
# layout
layout = None

def __init__(self, output, runner, display_name="", user=None, channel=None):
def __init__(self, output, runner, display_name="", user=None, channel=None, requester=None,
config=None):
# an output stream (writeln, info, warn error)
self.output = ScopedOutput(display_name, output)
self.display_name = display_name
Expand All @@ -143,6 +144,8 @@ def __init__(self, output, runner, display_name="", user=None, channel=None):

self.compatible_packages = []
self._conan_using_build_profile = False
self._conan_requester = requester
self._conan_config = config

self.layout = Layout()

Expand Down
71 changes: 71 additions & 0 deletions conans/test/functional/tools/test_files.py
@@ -0,0 +1,71 @@
import os
import textwrap

from bottle import static_file, request

from conans.test.utils.test_files import temp_folder
from conans.test.utils.tools import TestClient, StoppableThreadBottle
from conans.util.files import save


class TestConanToolFiles:

def test_imports(self):
conanfile = textwrap.dedent("""
czoido marked this conversation as resolved.
Show resolved Hide resolved
from conans import ConanFile
from conan.tools.files import load, save, mkdir, download, get, ftp_download

class Pkg(ConanFile):
pass
""")
client = TestClient()
client.save({"conanfile.py": conanfile})
client.run("install .")

def test_load_save_mkdir(self):
conanfile = textwrap.dedent("""
from conans import ConanFile
from conan.tools.files import load, save, mkdir

class Pkg(ConanFile):
name = "mypkg"
version = "1.0"
def source(self):
mkdir(self, "myfolder")
save(self, "./myfolder/myfile", "some_content")
assert load(self, "./myfolder/myfile") == "some_content"
""")

client = TestClient()
client.save({"conanfile.py": conanfile})
client.run("source .")

def test_download(self):
http_server = StoppableThreadBottle()
file_path = os.path.join(temp_folder(), "myfile.txt")
save(file_path, "some content")

@http_server.server.get("/myfile.txt")
def get_file():
return static_file(os.path.basename(file_path), os.path.dirname(file_path))

http_server.run_server()

conanfile = textwrap.dedent("""
from conans import ConanFile
from conan.tools.files import download

class Pkg(ConanFile):
name = "mypkg"
version = "1.0"
def source(self):
download(self,
"http://localhost:{}/myfile.txt",
"myfile.txt")
""".format(http_server.port))

client = TestClient()
client.save({"conanfile.py": conanfile})
client.run("source .")
local_path = os.path.join(client.current_folder, "myfile.txt")
assert os.path.exists(local_path)