refactor(api)!: Unifying log under 1 logger and removing debug parameter

So now you control the logging and the debugging from the shell through the logger with the name 'scrapling'
This commit is contained in:
Karim shoair
2024-12-11 21:41:37 +02:00
parent f30eb6ab6c
commit 193827e27b
16 changed files with 81 additions and 92 deletions
+1 -1
View File
@@ -65,7 +65,7 @@ body:
- type: textarea - type: textarea
attributes: attributes:
label: "Actual behavior (Remember to use `debug` parameter)" label: "Actual behavior"
validations: validations:
required: true required: true
+5 -1
View File
@@ -19,7 +19,11 @@ tests/test_parser_functions.py ................ [100%]
=============================== 16 passed in 0.22s ================================ =============================== 16 passed in 0.22s ================================
``` ```
Also, consider setting `debug` to `True` while initializing the Adaptor object so it's easier to know what's happening in the background. Also, consider setting the scrapling logging level to `debug` so it's easier to know what's happening in the background.
```python
>>> import logging
>>> logging.getLogger("scrapling").setLevel(logging.DEBUG)
```
### The process is straight-forward. ### The process is straight-forward.
+1 -1
View File
@@ -219,7 +219,7 @@ You might be slightly confused by now so let me clear things up. All fetcher-typ
```python ```python
from scrapling import Fetcher, StealthyFetcher, PlayWrightFetcher from scrapling import Fetcher, StealthyFetcher, PlayWrightFetcher
``` ```
All of them can take these initialization arguments: `auto_match`, `huge_tree`, `keep_comments`, `keep_cdata`, `storage`, `storage_args`, and `debug`, which are the same ones you give to the `Adaptor` class. All of them can take these initialization arguments: `auto_match`, `huge_tree`, `keep_comments`, `keep_cdata`, `storage`, and `storage_args`, which are the same ones you give to the `Adaptor` class.
If you don't want to pass arguments to the generated `Adaptor` object and want to use the default values, you can use this import instead for cleaner code: If you don't want to pass arguments to the generated `Adaptor` object and want to use the default values, you can use this import instead for cleaner code:
```python ```python
+3 -3
View File
@@ -64,9 +64,9 @@ def test_pyquery():
@benchmark @benchmark
def test_scrapling(): def test_scrapling():
# No need to do `.extract()` like parsel to extract text # No need to do `.extract()` like parsel to extract text
# Also, this is faster than `[t.text for t in Adaptor(large_html, auto_match=False, debug=False).css('.item')]` # Also, this is faster than `[t.text for t in Adaptor(large_html, auto_match=False).css('.item')]`
# for obvious reasons, of course. # for obvious reasons, of course.
return Adaptor(large_html, auto_match=False, debug=False).css('.item::text') return Adaptor(large_html, auto_match=False).css('.item::text')
@benchmark @benchmark
@@ -103,7 +103,7 @@ def test_scrapling_text(request_html):
# Will loop over resulted elements to get text too to make comparison even more fair otherwise Scrapling will be even faster # Will loop over resulted elements to get text too to make comparison even more fair otherwise Scrapling will be even faster
return [ return [
element.text for element in Adaptor( element.text for element in Adaptor(
request_html, auto_match=False, debug=False request_html, auto_match=False
).find_by_text('Tipping the Velvet', first_match=True).find_similar(ignore_attributes=['title']) ).find_by_text('Tipping the Velvet', first_match=True).find_similar(ignore_attributes=['title'])
] ]
+5 -6
View File
@@ -1,4 +1,3 @@
import logging
import sqlite3 import sqlite3
import threading import threading
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
@@ -9,7 +8,7 @@ from lxml import html
from tldextract import extract as tld from tldextract import extract as tld
from scrapling.core._types import Dict, Optional, Union from scrapling.core._types import Dict, Optional, Union
from scrapling.core.utils import _StorageTools, cache from scrapling.core.utils import _StorageTools, log, lru_cache
class StorageSystemMixin(ABC): class StorageSystemMixin(ABC):
@@ -20,7 +19,7 @@ class StorageSystemMixin(ABC):
""" """
self.url = url self.url = url
@cache(None, typed=True) @lru_cache(None, typed=True)
def _get_base_url(self, default_value: str = 'default') -> str: def _get_base_url(self, default_value: str = 'default') -> str:
if not self.url or type(self.url) is not str: if not self.url or type(self.url) is not str:
return default_value return default_value
@@ -52,7 +51,7 @@ class StorageSystemMixin(ABC):
raise NotImplementedError('Storage system must implement `save` method') raise NotImplementedError('Storage system must implement `save` method')
@staticmethod @staticmethod
@cache(None, typed=True) @lru_cache(None, typed=True)
def _get_hash(identifier: str) -> str: def _get_hash(identifier: str) -> str:
"""If you want to hash identifier in your storage system, use this safer""" """If you want to hash identifier in your storage system, use this safer"""
identifier = identifier.lower().strip() identifier = identifier.lower().strip()
@@ -64,7 +63,7 @@ class StorageSystemMixin(ABC):
return f"{hash_value}_{len(identifier)}" # Length to reduce collision chance return f"{hash_value}_{len(identifier)}" # Length to reduce collision chance
@cache(None, typed=True) @lru_cache(None, typed=True)
class SQLiteStorageSystem(StorageSystemMixin): class SQLiteStorageSystem(StorageSystemMixin):
"""The recommended system to use, it's race condition safe and thread safe. """The recommended system to use, it's race condition safe and thread safe.
Mainly built so the library can run in threaded frameworks like scrapy or threaded tools Mainly built so the library can run in threaded frameworks like scrapy or threaded tools
@@ -86,7 +85,7 @@ class SQLiteStorageSystem(StorageSystemMixin):
self.connection.execute("PRAGMA journal_mode=WAL") self.connection.execute("PRAGMA journal_mode=WAL")
self.cursor = self.connection.cursor() self.cursor = self.connection.cursor()
self._setup_database() self._setup_database()
logging.debug( log.debug(
f'Storage system loaded with arguments (storage_file="{storage_file}", url="{url}")' f'Storage system loaded with arguments (storage_file="{storage_file}", url="{url}")'
) )
+2 -2
View File
@@ -17,7 +17,7 @@ from cssselect.xpath import XPathExpr as OriginalXPathExpr
from w3lib.html import HTML5_WHITESPACE from w3lib.html import HTML5_WHITESPACE
from scrapling.core._types import Any, Optional, Protocol, Self from scrapling.core._types import Any, Optional, Protocol, Self
from scrapling.core.utils import cache from scrapling.core.utils import lru_cache
regex = f"[{HTML5_WHITESPACE}]+" regex = f"[{HTML5_WHITESPACE}]+"
replace_html5_whitespaces = re.compile(regex).sub replace_html5_whitespaces = re.compile(regex).sub
@@ -139,6 +139,6 @@ class TranslatorMixin:
class HTMLTranslator(TranslatorMixin, OriginalHTMLTranslator): class HTMLTranslator(TranslatorMixin, OriginalHTMLTranslator):
@cache(maxsize=256) @lru_cache(maxsize=256)
def css_to_xpath(self, css: str, prefix: str = "descendant-or-self::") -> str: def css_to_xpath(self, css: str, prefix: str = "descendant-or-self::") -> str:
return super().css_to_xpath(css, prefix) return super().css_to_xpath(css, prefix)
+29 -28
View File
@@ -9,18 +9,36 @@ from scrapling.core._types import Any, Dict, Iterable, Union
# Using cache on top of a class is brilliant way to achieve Singleton design pattern without much code # Using cache on top of a class is brilliant way to achieve Singleton design pattern without much code
# functools.cache is available on Python 3.9+ only so let's keep lru_cache # functools.cache is available on Python 3.9+ only so let's keep lru_cache
from functools import lru_cache as cache # isort:skip from functools import lru_cache # isort:skip
html_forbidden = {html.HtmlComment, } html_forbidden = {html.HtmlComment, }
logging.basicConfig(
level=logging.INFO,
format="[%(asctime)s] %(levelname)s: %(message)s", @lru_cache(1, typed=True)
datefmt="%Y-%m-%d %H:%M:%S", def setup_logger():
handlers=[ """Create and configure a logger with a standard format.
logging.StreamHandler()
] :returns: logging.Logger: Configured logger instance
) """
logger = logging.getLogger('scrapling')
logger.setLevel(logging.INFO)
formatter = logging.Formatter(
fmt="[%(asctime)s] %(levelname)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
# Add handler to logger (if not already added)
if not logger.handlers:
logger.addHandler(console_handler)
return logger
log = setup_logger()
def is_jsonable(content: Union[bytes, str]) -> bool: def is_jsonable(content: Union[bytes, str]) -> bool:
@@ -34,23 +52,6 @@ def is_jsonable(content: Union[bytes, str]) -> bool:
return False return False
@cache(None, typed=True)
def setup_basic_logging(level: str = 'debug'):
levels = {
'debug': logging.DEBUG,
'info': logging.INFO,
'warning': logging.WARNING,
'error': logging.ERROR,
'critical': logging.CRITICAL
}
formatter = logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s", "%Y-%m-%d %H:%M:%S")
lvl = levels[level.lower()]
handler = logging.StreamHandler()
handler.setFormatter(formatter)
# Configure the root logger
logging.basicConfig(level=lvl, handlers=[handler])
def flatten(lst: Iterable): def flatten(lst: Iterable):
return list(chain.from_iterable(lst)) return list(chain.from_iterable(lst))
@@ -114,7 +115,7 @@ class _StorageTools:
# return _impl # return _impl
@cache(None, typed=True) @lru_cache(None, typed=True)
def clean_spaces(string): def clean_spaces(string):
string = string.replace('\t', ' ') string = string.replace('\t', ' ')
string = re.sub('[\n|\r]', '', string) string = re.sub('[\n|\r]', '', string)
+2 -3
View File
@@ -1,10 +1,9 @@
import logging
from camoufox import DefaultAddons from camoufox import DefaultAddons
from camoufox.sync_api import Camoufox from camoufox.sync_api import Camoufox
from scrapling.core._types import (Callable, Dict, List, Literal, Optional, from scrapling.core._types import (Callable, Dict, List, Literal, Optional,
Union) Union)
from scrapling.core.utils import log
from scrapling.engines.toolbelt import (Response, StatusText, from scrapling.engines.toolbelt import (Response, StatusText,
check_type_validity, check_type_validity,
construct_proxy_dict, do_nothing, construct_proxy_dict, do_nothing,
@@ -63,7 +62,7 @@ class CamoufoxEngine:
self.page_action = page_action self.page_action = page_action
else: else:
self.page_action = do_nothing self.page_action = do_nothing
logging.error('[Ignored] Argument "page_action" must be callable') log.error('[Ignored] Argument "page_action" must be callable')
self.wait_selector = wait_selector self.wait_selector = wait_selector
self.wait_selector_state = wait_selector_state self.wait_selector_state = wait_selector_state
+2 -2
View File
@@ -1,7 +1,7 @@
import json import json
import logging
from scrapling.core._types import Callable, Dict, List, Optional, Union from scrapling.core._types import Callable, Dict, List, Optional, Union
from scrapling.core.utils import log
from scrapling.engines.constants import (DEFAULT_STEALTH_FLAGS, from scrapling.engines.constants import (DEFAULT_STEALTH_FLAGS,
NSTBROWSER_DEFAULT_QUERY) NSTBROWSER_DEFAULT_QUERY)
from scrapling.engines.toolbelt import (Response, StatusText, from scrapling.engines.toolbelt import (Response, StatusText,
@@ -78,7 +78,7 @@ class PlaywrightEngine:
self.page_action = page_action self.page_action = page_action
else: else:
self.page_action = do_nothing self.page_action = do_nothing
logging.error('[Ignored] Argument "page_action" must be callable') log.error('[Ignored] Argument "page_action" must be callable')
self.wait_selector = wait_selector self.wait_selector = wait_selector
self.wait_selector_state = wait_selector_state self.wait_selector_state = wait_selector_state
-3
View File
@@ -1,5 +1,3 @@
import logging
import httpx import httpx
from httpx._models import Response as httpxResponse from httpx._models import Response as httpxResponse
@@ -36,7 +34,6 @@ class StaticEngine:
# Validate headers # Validate headers
if not headers.get('user-agent') and not headers.get('User-Agent'): if not headers.get('user-agent') and not headers.get('User-Agent'):
headers['User-Agent'] = generate_headers(browser_mode=False).get('User-Agent') headers['User-Agent'] = generate_headers(browser_mode=False).get('User-Agent')
logging.info(f"Can't find useragent in headers so '{headers['User-Agent']}' was used.")
if stealth: if stealth:
extra_headers = generate_headers(browser_mode=False) extra_headers = generate_headers(browser_mode=False)
+10 -15
View File
@@ -2,13 +2,12 @@
Functions related to custom types or type checking Functions related to custom types or type checking
""" """
import inspect import inspect
import logging
from email.message import Message from email.message import Message
from scrapling.core._types import (Any, Callable, Dict, List, Optional, Tuple, from scrapling.core._types import (Any, Callable, Dict, List, Optional, Tuple,
Type, Union) Type, Union)
from scrapling.core.custom_types import MappingProxyType from scrapling.core.custom_types import MappingProxyType
from scrapling.core.utils import cache, setup_basic_logging from scrapling.core.utils import log, lru_cache
from scrapling.parser import Adaptor, SQLiteStorageSystem from scrapling.parser import Adaptor, SQLiteStorageSystem
@@ -17,7 +16,7 @@ class ResponseEncoding:
__ISO_8859_1_CONTENT_TYPES = {"text/plain", "text/html", "text/css", "text/javascript"} __ISO_8859_1_CONTENT_TYPES = {"text/plain", "text/html", "text/css", "text/javascript"}
@classmethod @classmethod
@cache(maxsize=None) @lru_cache(maxsize=None)
def __parse_content_type(cls, header_value: str) -> Tuple[str, Dict[str, str]]: def __parse_content_type(cls, header_value: str) -> Tuple[str, Dict[str, str]]:
"""Parse content type and parameters from a content-type header value. """Parse content type and parameters from a content-type header value.
@@ -39,7 +38,7 @@ class ResponseEncoding:
return content_type, params return content_type, params
@classmethod @classmethod
@cache(maxsize=None) @lru_cache(maxsize=None)
def get_value(cls, content_type: Optional[str], text: Optional[str] = 'test') -> str: def get_value(cls, content_type: Optional[str], text: Optional[str] = 'test') -> str:
"""Determine the appropriate character encoding from a content-type header. """Determine the appropriate character encoding from a content-type header.
@@ -98,7 +97,7 @@ class Response(Adaptor):
# For back-ward compatibility # For back-ward compatibility
self.adaptor = self self.adaptor = self
# For easier debugging while working from a Python shell # For easier debugging while working from a Python shell
logging.info(f'Fetched ({status}) <{method} {url}> (referer: {request_headers.get("referer")})') log.info(f'Fetched ({status}) <{method} {url}> (referer: {request_headers.get("referer")})')
# def __repr__(self): # def __repr__(self):
# return f'<{self.__class__.__name__} [{self.status} {self.reason}]>' # return f'<{self.__class__.__name__} [{self.status} {self.reason}]>'
@@ -107,7 +106,7 @@ class Response(Adaptor):
class BaseFetcher: class BaseFetcher:
def __init__( def __init__(
self, huge_tree: bool = True, keep_comments: Optional[bool] = False, auto_match: Optional[bool] = True, self, huge_tree: bool = True, keep_comments: Optional[bool] = False, auto_match: Optional[bool] = True,
storage: Any = SQLiteStorageSystem, storage_args: Optional[Dict] = None, debug: Optional[bool] = False, storage: Any = SQLiteStorageSystem, storage_args: Optional[Dict] = None,
automatch_domain: Optional[str] = None, keep_cdata: Optional[bool] = False, automatch_domain: Optional[str] = None, keep_cdata: Optional[bool] = False,
): ):
"""Arguments below are the same from the Adaptor class so you can pass them directly, the rest of Adaptor's arguments """Arguments below are the same from the Adaptor class so you can pass them directly, the rest of Adaptor's arguments
@@ -124,7 +123,6 @@ class BaseFetcher:
If empty, default values will be used. If empty, default values will be used.
:param automatch_domain: For cases where you want to automatch selectors across different websites as if they were on the same website, use this argument to unify them. :param automatch_domain: For cases where you want to automatch selectors across different websites as if they were on the same website, use this argument to unify them.
Otherwise, the domain of the request is used by default. Otherwise, the domain of the request is used by default.
:param debug: Enable debug mode
""" """
# Adaptor class parameters # Adaptor class parameters
# I won't validate Adaptor's class parameters here again, I will leave it to be validated later # I won't validate Adaptor's class parameters here again, I will leave it to be validated later
@@ -134,14 +132,11 @@ class BaseFetcher:
keep_cdata=keep_cdata, keep_cdata=keep_cdata,
auto_match=auto_match, auto_match=auto_match,
storage=storage, storage=storage,
storage_args=storage_args, storage_args=storage_args
debug=debug,
) )
# If the user used fetchers first, then configure the logger from here instead of the `Adaptor` class
setup_basic_logging(level='debug' if debug else 'info')
if automatch_domain: if automatch_domain:
if type(automatch_domain) is not str: if type(automatch_domain) is not str:
logging.warning('[Ignored] The argument "automatch_domain" must be of string type') log.warning('[Ignored] The argument "automatch_domain" must be of string type')
else: else:
self.adaptor_arguments.update({'automatch_domain': automatch_domain}) self.adaptor_arguments.update({'automatch_domain': automatch_domain})
@@ -217,7 +212,7 @@ class StatusText:
}) })
@classmethod @classmethod
@cache(maxsize=128) @lru_cache(maxsize=128)
def get(cls, status_code: int) -> str: def get(cls, status_code: int) -> str:
"""Get the phrase for a given HTTP status code.""" """Get the phrase for a given HTTP status code."""
return cls._phrases.get(status_code, "Unknown Status Code") return cls._phrases.get(status_code, "Unknown Status Code")
@@ -284,7 +279,7 @@ def check_type_validity(variable: Any, valid_types: Union[List[Type], None], def
error_msg = f'Argument "{var_name}" cannot be None' error_msg = f'Argument "{var_name}" cannot be None'
if critical: if critical:
raise TypeError(error_msg) raise TypeError(error_msg)
logging.error(f'[Ignored] {error_msg}') log.error(f'[Ignored] {error_msg}')
return default_value return default_value
# If no valid_types specified and variable has a value, return it # If no valid_types specified and variable has a value, return it
@@ -297,7 +292,7 @@ def check_type_validity(variable: Any, valid_types: Union[List[Type], None], def
error_msg = f'Argument "{var_name}" must be of type {" or ".join(type_names)}' error_msg = f'Argument "{var_name}" must be of type {" or ".join(type_names)}'
if critical: if critical:
raise TypeError(error_msg) raise TypeError(error_msg)
logging.error(f'[Ignored] {error_msg}') log.error(f'[Ignored] {error_msg}')
return default_value return default_value
return variable return variable
+3 -3
View File
@@ -9,10 +9,10 @@ from browserforge.headers import Browser, HeaderGenerator
from tldextract import extract from tldextract import extract
from scrapling.core._types import Dict, Union from scrapling.core._types import Dict, Union
from scrapling.core.utils import cache from scrapling.core.utils import lru_cache
@cache(None, typed=True) @lru_cache(None, typed=True)
def generate_convincing_referer(url: str) -> str: def generate_convincing_referer(url: str) -> str:
"""Takes the domain from the URL without the subdomain/suffix and make it look like you were searching google for this website """Takes the domain from the URL without the subdomain/suffix and make it look like you were searching google for this website
@@ -26,7 +26,7 @@ def generate_convincing_referer(url: str) -> str:
return f'https://www.google.com/search?q={website_name}' return f'https://www.google.com/search?q={website_name}'
@cache(None, typed=True) @lru_cache(None, typed=True)
def get_os_name() -> Union[str, None]: def get_os_name() -> Union[str, None]:
"""Get the current OS name in the same format needed for browserforge """Get the current OS name in the same format needed for browserforge
+3 -5
View File
@@ -1,15 +1,13 @@
""" """
Functions related to files and URLs Functions related to files and URLs
""" """
import logging
import os import os
from urllib.parse import urlencode, urlparse from urllib.parse import urlencode, urlparse
from playwright.sync_api import Route from playwright.sync_api import Route
from scrapling.core._types import Dict, Optional, Union from scrapling.core._types import Dict, Optional, Union
from scrapling.core.utils import cache from scrapling.core.utils import log, lru_cache
from scrapling.engines.constants import DEFAULT_DISABLED_RESOURCES from scrapling.engines.constants import DEFAULT_DISABLED_RESOURCES
@@ -20,7 +18,7 @@ def intercept_route(route: Route) -> Union[Route, None]:
:return: PlayWright `Route` object :return: PlayWright `Route` object
""" """
if route.request.resource_type in DEFAULT_DISABLED_RESOURCES: if route.request.resource_type in DEFAULT_DISABLED_RESOURCES:
logging.debug(f'Blocking background resource "{route.request.url}" of type "{route.request.resource_type}"') log.debug(f'Blocking background resource "{route.request.url}" of type "{route.request.resource_type}"')
return route.abort() return route.abort()
return route.continue_() return route.continue_()
@@ -97,7 +95,7 @@ def construct_cdp_url(cdp_url: str, query_params: Optional[Dict] = None) -> str:
raise ValueError(f"Invalid CDP URL: {str(e)}") raise ValueError(f"Invalid CDP URL: {str(e)}")
@cache(None, typed=True) @lru_cache(None, typed=True)
def js_bypass_path(filename: str) -> str: def js_bypass_path(filename: str) -> str:
"""Takes the base filename of JS file inside the `bypasses` folder then return the full path of it """Takes the base filename of JS file inside the `bypasses` folder then return the full path of it
+11 -15
View File
@@ -18,12 +18,12 @@ from scrapling.core.storage_adaptors import (SQLiteStorageSystem,
StorageSystemMixin, _StorageTools) StorageSystemMixin, _StorageTools)
from scrapling.core.translator import HTMLTranslator from scrapling.core.translator import HTMLTranslator
from scrapling.core.utils import (clean_spaces, flatten, html_forbidden, from scrapling.core.utils import (clean_spaces, flatten, html_forbidden,
is_jsonable, logging, setup_basic_logging) is_jsonable, log)
class Adaptor(SelectorsGeneration): class Adaptor(SelectorsGeneration):
__slots__ = ( __slots__ = (
'url', 'encoding', '__auto_match_enabled', '_root', '_storage', '__debug', 'url', 'encoding', '__auto_match_enabled', '_root', '_storage',
'__keep_comments', '__huge_tree_enabled', '__attributes', '__text', '__tag', '__keep_comments', '__huge_tree_enabled', '__attributes', '__text', '__tag',
'__keep_cdata', '__raw_body' '__keep_cdata', '__raw_body'
) )
@@ -41,7 +41,6 @@ class Adaptor(SelectorsGeneration):
auto_match: Optional[bool] = True, auto_match: Optional[bool] = True,
storage: Any = SQLiteStorageSystem, storage: Any = SQLiteStorageSystem,
storage_args: Optional[Dict] = None, storage_args: Optional[Dict] = None,
debug: Optional[bool] = True,
**kwargs **kwargs
): ):
"""The main class that works as a wrapper for the HTML input data. Using this class, you can search for elements """The main class that works as a wrapper for the HTML input data. Using this class, you can search for elements
@@ -67,7 +66,6 @@ class Adaptor(SelectorsGeneration):
:param storage: The storage class to be passed for auto-matching functionalities, see ``Docs`` for more info. :param storage: The storage class to be passed for auto-matching functionalities, see ``Docs`` for more info.
:param storage_args: A dictionary of ``argument->value`` pairs to be passed for the storage class. :param storage_args: A dictionary of ``argument->value`` pairs to be passed for the storage class.
If empty, default values will be used. If empty, default values will be used.
:param debug: Enable debug mode
""" """
if root is None and not body and text is None: if root is None and not body and text is None:
raise ValueError("Adaptor class needs text, body, or root arguments to work") raise ValueError("Adaptor class needs text, body, or root arguments to work")
@@ -106,7 +104,6 @@ class Adaptor(SelectorsGeneration):
self._root = root self._root = root
setup_basic_logging(level='debug' if debug else 'info')
self.__auto_match_enabled = auto_match self.__auto_match_enabled = auto_match
if self.__auto_match_enabled: if self.__auto_match_enabled:
@@ -117,7 +114,7 @@ class Adaptor(SelectorsGeneration):
} }
if not hasattr(storage, '__wrapped__'): if not hasattr(storage, '__wrapped__'):
raise ValueError("Storage class must be wrapped with cache decorator, see docs for info") raise ValueError("Storage class must be wrapped with lru_cache decorator, see docs for info")
if not issubclass(storage.__wrapped__, StorageSystemMixin): if not issubclass(storage.__wrapped__, StorageSystemMixin):
raise ValueError("Storage system must be inherited from class `StorageSystemMixin`") raise ValueError("Storage system must be inherited from class `StorageSystemMixin`")
@@ -132,7 +129,6 @@ class Adaptor(SelectorsGeneration):
# For selector stuff # For selector stuff
self.__attributes = None self.__attributes = None
self.__tag = None self.__tag = None
self.__debug = debug
# No need to check if all response attributes exist or not because if `status` exist, then the rest exist (Save some CPU cycles for speed) # No need to check if all response attributes exist or not because if `status` exist, then the rest exist (Save some CPU cycles for speed)
self.__response_data = { self.__response_data = {
key: getattr(self, key) for key in ('status', 'reason', 'cookies', 'headers', 'request_headers',) key: getattr(self, key) for key in ('status', 'reason', 'cookies', 'headers', 'request_headers',)
@@ -164,7 +160,7 @@ class Adaptor(SelectorsGeneration):
text='', body=b'', # Since root argument is provided, both `text` and `body` will be ignored so this is just a filler text='', body=b'', # Since root argument is provided, both `text` and `body` will be ignored so this is just a filler
url=self.url, encoding=self.encoding, auto_match=self.__auto_match_enabled, url=self.url, encoding=self.encoding, auto_match=self.__auto_match_enabled,
keep_comments=self.__keep_comments, keep_cdata=self.__keep_cdata, keep_comments=self.__keep_comments, keep_cdata=self.__keep_cdata,
huge_tree=self.__huge_tree_enabled, debug=self.__debug, huge_tree=self.__huge_tree_enabled,
**self.__response_data **self.__response_data
) )
return element return element
@@ -417,10 +413,10 @@ class Adaptor(SelectorsGeneration):
if score_table: if score_table:
highest_probability = max(score_table.keys()) highest_probability = max(score_table.keys())
if score_table[highest_probability] and highest_probability >= percentage: if score_table[highest_probability] and highest_probability >= percentage:
logging.debug(f'Highest probability was {highest_probability}%') log.debug(f'Highest probability was {highest_probability}%')
logging.debug('Top 5 best matching elements are: ') log.debug('Top 5 best matching elements are: ')
for percent in tuple(sorted(score_table.keys(), reverse=True))[:5]: for percent in tuple(sorted(score_table.keys(), reverse=True))[:5]:
logging.debug(f'{percent} -> {self.__convert_results(score_table[percent])}') log.debug(f'{percent} -> {self.__convert_results(score_table[percent])}')
if not adaptor_type: if not adaptor_type:
return score_table[highest_probability] return score_table[highest_probability]
return self.__convert_results(score_table[highest_probability]) return self.__convert_results(score_table[highest_probability])
@@ -546,7 +542,7 @@ class Adaptor(SelectorsGeneration):
if selected_elements: if selected_elements:
if not self.__auto_match_enabled and auto_save: if not self.__auto_match_enabled and auto_save:
logging.warning("Argument `auto_save` will be ignored because `auto_match` wasn't enabled on initialization. Check docs for more info.") log.warning("Argument `auto_save` will be ignored because `auto_match` wasn't enabled on initialization. Check docs for more info.")
elif self.__auto_match_enabled and auto_save: elif self.__auto_match_enabled and auto_save:
self.save(selected_elements[0], identifier or selector) self.save(selected_elements[0], identifier or selector)
@@ -565,7 +561,7 @@ class Adaptor(SelectorsGeneration):
return self.__convert_results(selected_elements) return self.__convert_results(selected_elements)
elif not self.__auto_match_enabled and auto_match: elif not self.__auto_match_enabled and auto_match:
logging.warning("Argument `auto_match` will be ignored because `auto_match` wasn't enabled on initialization. Check docs for more info.") log.warning("Argument `auto_match` will be ignored because `auto_match` wasn't enabled on initialization. Check docs for more info.")
return self.__convert_results(selected_elements) return self.__convert_results(selected_elements)
@@ -769,7 +765,7 @@ class Adaptor(SelectorsGeneration):
self._storage.save(element, identifier) self._storage.save(element, identifier)
else: else:
logging.critical( log.critical(
"Can't use Auto-match features with disabled globally, you have to start a new class instance." "Can't use Auto-match features with disabled globally, you have to start a new class instance."
) )
@@ -783,7 +779,7 @@ class Adaptor(SelectorsGeneration):
if self.__auto_match_enabled: if self.__auto_match_enabled:
return self._storage.retrieve(identifier) return self._storage.retrieve(identifier)
logging.critical( log.critical(
"Can't use Auto-match features with disabled globally, you have to start a new class instance." "Can't use Auto-match features with disabled globally, you have to start a new class instance."
) )
+2 -2
View File
@@ -42,8 +42,8 @@ class TestParserAutoMatch(unittest.TestCase):
</div> </div>
''' '''
old_page = Adaptor(original_html, url='example.com', auto_match=True, debug=True) old_page = Adaptor(original_html, url='example.com', auto_match=True)
new_page = Adaptor(changed_html, url='example.com', auto_match=True, debug=True) new_page = Adaptor(changed_html, url='example.com', auto_match=True)
# 'p1' was used as ID and now it's not and all the path elements have changes # 'p1' was used as ID and now it's not and all the path elements have changes
# Also at the same time testing auto-match vs combined selectors # Also at the same time testing auto-match vs combined selectors
+2 -2
View File
@@ -74,7 +74,7 @@ class TestParser(unittest.TestCase):
</body> </body>
</html> </html>
''' '''
self.page = Adaptor(self.html, auto_match=False, debug=False) self.page = Adaptor(self.html, auto_match=False)
def test_css_selector(self): def test_css_selector(self):
"""Test Selecting elements with complex CSS selectors""" """Test Selecting elements with complex CSS selectors"""
@@ -273,7 +273,7 @@ class TestParser(unittest.TestCase):
large_html = '<html><body>' + '<div class="item">' * 5000 + '</div>' * 5000 + '</body></html>' large_html = '<html><body>' + '<div class="item">' * 5000 + '</div>' * 5000 + '</body></html>'
start_time = time.time() start_time = time.time()
parsed = Adaptor(large_html, auto_match=False, debug=False) parsed = Adaptor(large_html, auto_match=False)
elements = parsed.css('.item') elements = parsed.css('.item')
end_time = time.time() end_time = time.time()