python-adblock

Updated Brave adblock library for Python
Log | Files | Refs | README

lib.rs (24276B)


      1 //! Python wrapper for Brave's adblocking library, which is written in Rust.
      2 #![deny(
      3     future_incompatible,
      4     nonstandard_style,
      5     rust_2018_idioms,
      6     missing_copy_implementations,
      7     trivial_casts,
      8     trivial_numeric_casts,
      9     unsafe_code,
     10     unused_qualifications,
     11     deprecated
     12 )]
     13 
     14 use adblock::blocker::BlockerResult as RustBlockerResult;
     15 use adblock::cosmetic_filter_cache::UrlSpecificResources as RustUrlSpecificResources;
     16 use adblock::engine::Engine as RustEngine;
     17 use adblock::lists::FilterSet as RustFilterSet;
     18 use adblock::lists::{FilterFormat, ParseOptions, RuleTypes};
     19 use adblock::request::Request;
     20 use pyo3::create_exception;
     21 use pyo3::exceptions::PyException;
     22 use pyo3::prelude::*;
     23 use pyo3::types::PyBytes;
     24 use pyo3::PyErr;
     25 
     26 use adblock::resources::{MimeType, PermissionMask, Resource, ResourceType};
     27 use std::collections::HashSet;
     28 use std::error::Error;
     29 use std::fmt::{self, Display};
     30 use std::fs;
     31 use std::io::{Read, Write};
     32 
     33 /// Brave's adblocking library in Python!
     34 #[pymodule]
     35 fn adblock_py(m: &Bound<'_, PyModule>) -> PyResult<()> {
     36     m.add("__version__", env!("CARGO_PKG_VERSION"))?;
     37     m.add_class::<Engine>()?;
     38     m.add_class::<FilterSet>()?;
     39     m.add_class::<BlockerResult>()?;
     40     m.add_class::<UrlSpecificResources>()?;
     41     m.add("AdblockException", m.py().get_type_bound::<AdblockException>())?;
     42     m.add("BlockerException", m.py().get_type_bound::<BlockerException>())?;
     43     m.add("SerializationError", m.py().get_type_bound::<SerializationError>())?;
     44     m.add(
     45         "DeserializationError",
     46         m.py().get_type_bound::<DeserializationError>(),
     47     )?;
     48     m.add(
     49         "OptimizedFilterExistence",
     50         m.py().get_type_bound::<OptimizedFilterExistence>(),
     51     )?;
     52     m.add(
     53         "BadFilterAddUnsupported",
     54         m.py().get_type_bound::<BadFilterAddUnsupported>(),
     55     )?;
     56     m.add("FilterExists", m.py().get_type_bound::<FilterExists>())?;
     57     m.add(
     58         "AddResourceException",
     59         m.py().get_type_bound::<AddResourceException>(),
     60     )?;
     61     m.add(
     62         "InvalidBase64ContentError",
     63         m.py().get_type_bound::<InvalidBase64ContentError>(),
     64     )?;
     65     m.add(
     66         "InvalidUtf8ContentError",
     67         m.py().get_type_bound::<InvalidUtf8ContentError>(),
     68     )?;
     69     Ok(())
     70 }
     71 
     72 /// The result of an ad-blocking check.
     73 #[pyclass]
     74 pub struct BlockerResult {
     75     #[pyo3(get)]
     76     pub matched: bool,
     77     /// Important is used to signal that a rule with the `important` option
     78     /// matched. An `important` match means that exceptions should not apply
     79     /// and no further checking is neccesary--the request should be blocked
     80     /// (empty body or cancelled).
     81     ///
     82     /// Brave Browser keeps seperate instances of Blocker for default lists
     83     /// and regional ones, so `important` here is used to correct behaviour
     84     /// between them: checking should stop instead of moving to the next
     85     /// instance iff an `important` rule matched.
     86     #[pyo3(get)]
     87     pub important: bool,
     88     /// Iff the blocker matches a rule which has the `redirect` option, as per
     89     /// [uBlock Origin's redirect syntax][1], the `redirect` is not `None`.
     90     /// The `redirect` field contains the body of the redirect to be injected.
     91     ///
     92     /// [1]: https://github.com/gorhill/uBlock/wiki/Static-filter-syntax#redirect
     93     #[pyo3(get)]
     94     pub redirect: Option<String>,
     95     /// `removeparam` may remove URL parameters. If the original request URL was
     96     /// modified at all, the new version will be here.
     97     #[pyo3(get)]
     98     pub rewritten_url: Option<String>,
     99     /// Exception is not `None` when the blocker matched on an exception rule.
    100     /// Effectively this means that there was a match, but the request should
    101     /// not be blocked. It is a non-empty string if the blocker was initialized
    102     /// from a list of rules with debugging enabled, otherwise the original
    103     /// string representation is discarded to reduce memory use.
    104     #[pyo3(get)]
    105     pub exception: Option<String>,
    106     /// Filter--similarly to exception--includes the string representation of
    107     /// the rule when there is a match and debugging is enabled. Otherwise, on
    108     /// a match, it is not `None`.
    109     #[pyo3(get)]
    110     pub filter: Option<String>,
    111 }
    112 
    113 impl From<RustBlockerResult> for BlockerResult {
    114     fn from(br: RustBlockerResult) -> Self {
    115         Self {
    116             matched: br.matched,
    117             important: br.important,
    118             redirect: br.redirect,
    119             rewritten_url: br.rewritten_url,
    120             exception: br.exception,
    121             filter: br.filter,
    122         }
    123     }
    124 }
    125 
    126 #[pymethods]
    127 impl BlockerResult {
    128     fn __repr__(&self) -> PyResult<String> {
    129         Ok(format!(
    130             "BlockerResult(matched={}, important={}, redirect={}, rewritten_url={}, exception={}, filter={})",
    131             self.matched.diy_python_repr(),
    132             self.important.diy_python_repr(),
    133             self.redirect.diy_python_repr(),
    134             self.rewritten_url.diy_python_repr(),
    135             self.exception.diy_python_repr(),
    136             self.filter.diy_python_repr(),
    137         ))
    138     }
    139 }
    140 
    141 #[derive(Debug, PartialEq, Eq, Copy, Clone)]
    142 pub enum BlockerError {
    143     SerializationError,
    144     DeserializationError,
    145     OptimizedFilterExistence,
    146     BadFilterAddUnsupported,
    147     FilterExists,
    148 }
    149 
    150 impl Error for BlockerError {
    151     fn source(&self) -> Option<&(dyn Error + 'static)> {
    152         None
    153     }
    154 }
    155 
    156 impl Display for BlockerError {
    157     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    158         write!(
    159             f,
    160             "{}",
    161             match self {
    162                 Self::SerializationError => "Serialization error",
    163                 Self::DeserializationError => "Deserialization error",
    164                 Self::OptimizedFilterExistence => "Optimized filter exists",
    165                 Self::BadFilterAddUnsupported => "Bad filter add unsupported",
    166                 Self::FilterExists => "Filter exists",
    167             }
    168         )
    169     }
    170 }
    171 
    172 create_exception!(adblock, AdblockException, PyException);
    173 create_exception!(adblock, BlockerException, AdblockException);
    174 create_exception!(adblock, AddResourceException, AdblockException);
    175 create_exception!(adblock, InvalidBase64ContentError, AddResourceException);
    176 create_exception!(adblock, InvalidUtf8ContentError, AddResourceException);
    177 create_exception!(adblock, SerializationError, BlockerException);
    178 create_exception!(adblock, DeserializationError, BlockerException);
    179 create_exception!(adblock, OptimizedFilterExistence, BlockerException);
    180 create_exception!(adblock, BadFilterAddUnsupported, BlockerException);
    181 create_exception!(adblock, FilterExists, BlockerException);
    182 
    183 impl From<BlockerError> for PyErr {
    184     fn from(err: BlockerError) -> Self {
    185         let msg = format!("{:?}", err);
    186         match err {
    187             BlockerError::SerializationError => Self::new::<SerializationError, _>(msg),
    188             BlockerError::DeserializationError => Self::new::<DeserializationError, _>(msg),
    189             BlockerError::OptimizedFilterExistence => Self::new::<OptimizedFilterExistence, _>(msg),
    190             BlockerError::BadFilterAddUnsupported => Self::new::<BadFilterAddUnsupported, _>(msg),
    191             BlockerError::FilterExists => Self::new::<FilterExists, _>(msg),
    192         }
    193     }
    194 }
    195 
    196 
    197 fn filter_format_from_string(filter_format: &str) -> PyResult<FilterFormat> {
    198     match filter_format {
    199         "standard" => Ok(FilterFormat::Standard),
    200         "hosts" => Ok(FilterFormat::Hosts),
    201         _ => Err(PyErr::new::<AdblockException, _>(
    202             "Invalid FilterFormat value",
    203         )),
    204     }
    205 }
    206 
    207 fn rule_types_from_string(rule_types: &str) -> PyResult<RuleTypes> {
    208     match rule_types {
    209         "all" => Ok(RuleTypes::All),
    210         "networkonly" => Ok(RuleTypes::NetworkOnly),
    211         "cosmeticonly" => Ok(RuleTypes::CosmeticOnly),
    212         _ => Err(PyErr::new::<AdblockException, _>("Invalid RuleTypes value")),
    213     }
    214 }
    215 
    216 /// Manages a set of rules to be added to an Engine.
    217 ///
    218 /// To be able to efficiently handle special options like $badfilter, and to
    219 /// allow optimizations, all rules must be available when the Engine is first
    220 /// created. FilterSet allows assembling a compound list from multiple
    221 /// different sources before compiling the rules into an Engine.
    222 #[pyclass]
    223 #[derive(Clone)]
    224 pub struct FilterSet {
    225     filter_set: RustFilterSet,
    226     debug: bool,
    227 }
    228 
    229 #[pymethods]
    230 impl FilterSet {
    231     /// Creates a new `FilterSet`. The `debug` argument specifies whether or
    232     /// not to save information about the original raw filter rules alongside
    233     /// the more compact internal representation. If enabled, this information
    234     /// will be passed to the corresponding Engine.
    235     #[new]
    236     #[pyo3(signature = (debug = false))]
    237     pub fn new(debug: bool) -> Self {
    238         Self {
    239             filter_set: RustFilterSet::new(debug),
    240             debug,
    241         }
    242     }
    243 
    244     /// Adds the contents of an entire filter list to this FilterSet. Filters
    245     /// that cannot be parsed successfully are ignored.
    246     ///
    247     /// The format is a string containing either "standard" (ABP/uBO-style)
    248     /// or "hosts".
    249     #[pyo3(signature = (filter_list, format = "standard", rule_types = "all"))]
    250     pub fn add_filter_list(
    251         &mut self,
    252         filter_list: &str,
    253         format: &str,
    254         rule_types: &str,
    255     ) -> PyResult<()> {
    256         let filter_format = filter_format_from_string(format)?;
    257         let rule_types = rule_types_from_string(rule_types)?;
    258         self.filter_set.add_filter_list(
    259             filter_list,
    260             ParseOptions {
    261                 format: filter_format,
    262                 rule_types,
    263                 permissions: PermissionMask::default(),
    264             },
    265         );
    266         Ok(())
    267     }
    268 
    269     /// Adds a collection of filter rules to this FilterSet. Filters that
    270     /// cannot be parsed successfully are ignored.
    271     ///
    272     /// The format is a string containing either "standard" (ABP/uBO-style)
    273     /// or "hosts".
    274     #[pyo3(signature = (filters, format = "standard", rule_types = "all"))]
    275     pub fn add_filters(
    276         &mut self,
    277         filters: Vec<String>,
    278         format: &str,
    279         rule_types: &str,
    280     ) -> PyResult<()> {
    281         let filter_format = filter_format_from_string(format)?;
    282         let rule_types = rule_types_from_string(rule_types)?;
    283         self.filter_set.add_filters(
    284             &filters,
    285             ParseOptions {
    286                 format: filter_format,
    287                 rule_types,
    288                 permissions: PermissionMask::default(),
    289             },
    290         );
    291         Ok(())
    292     }
    293 
    294     fn __repr__(&self) -> PyResult<String> {
    295         Ok(format!("FilterSet(debug={})", self.debug.diy_python_repr()))
    296     }
    297 }
    298 
    299 /// Contains cosmetic filter information intended to be injected into a
    300 /// particular hostname.
    301 #[pyclass]
    302 pub struct UrlSpecificResources {
    303     /// A set of any CSS selector on the page that should be hidden, i.e.
    304     /// styled as `{ display: none !important; }`.
    305     #[pyo3(get)]
    306     pub hide_selectors: HashSet<String>,
    307     /// Set of JSON-encoded procedural filters or filters with an action.
    308     #[pyo3(get)]
    309     pub procedural_actions: HashSet<String>,
    310     /// A set of any class or id CSS selectors that should not have generic
    311     /// rules applied.
    312     // In practice, these should be passed to `class_id_stylesheet` and not
    313     // used otherwise.
    314     #[pyo3(get)]
    315     pub exceptions: HashSet<String>,
    316     /// Javascript code for any scriptlets that should be injected into the
    317     /// page.
    318     #[pyo3(get)]
    319     pub injected_script: String,
    320     /// `generichide` is set to `True` if there is a corresponding
    321     /// `$generichide` exception network filter. If so, the page should not
    322     /// query for additional generic rules using hidden_class_id_selectors.
    323     #[pyo3(get)]
    324     pub generichide: bool,
    325 }
    326 
    327 impl From<RustUrlSpecificResources> for UrlSpecificResources {
    328     fn from(r: RustUrlSpecificResources) -> Self {
    329         Self {
    330             hide_selectors: r.hide_selectors,
    331             procedural_actions: r.procedural_actions,
    332             exceptions: r.exceptions,
    333             injected_script: r.injected_script,
    334             generichide: r.generichide,
    335         }
    336     }
    337 }
    338 
    339 #[pymethods]
    340 impl UrlSpecificResources {
    341     fn __repr__(&self) -> PyResult<String> {
    342         Ok(format!(
    343             "UrlSpecificResources<{} hide selectors, {} procedural actions, {} exceptions, injected_javascript={}, generichide={}>",
    344             self.hide_selectors.len(),
    345             self.procedural_actions.len(),
    346             self.exceptions.len(),
    347             self.injected_script.diy_python_repr(),
    348             self.generichide.diy_python_repr(),
    349         ))
    350     }
    351 }
    352 
    353 /// The main object featured in this library. This object holds the adblocker's
    354 /// state, and can be queried to see if a given request should be blocked or
    355 /// not.
    356 ///
    357 /// # Request types
    358 /// A few of `Engine`'s methods have a field specifying a "resource type",
    359 /// valid examples are:
    360 /// * `beacon`
    361 /// * `csp_report`
    362 /// * `document`
    363 /// * `font`
    364 /// * `media`
    365 /// * `object`
    366 /// * `script`
    367 /// * `stylesheet`
    368 /// * and et cetera...
    369 /// See the [Mozilla Web Documentation][1] for more info.
    370 ///
    371 /// [1]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/ResourceType
    372 #[pyclass]
    373 pub struct Engine {
    374     engine: RustEngine,
    375     optimize: bool,
    376 }
    377 
    378 #[pymethods]
    379 impl Engine {
    380     /// Create a new adblocking engine
    381     #[new]
    382     #[pyo3(signature = (filter_set, optimize = true))]
    383     pub fn new(filter_set: FilterSet, optimize: bool) -> Self {
    384         let engine = RustEngine::from_filter_set(filter_set.filter_set, optimize);
    385         Self { engine, optimize }
    386     }
    387 
    388     /// Check if the given `url`—pointing to a resource of type `request_type`—
    389     /// is blocked, assuming the request is made from the given `source_url`.
    390     /// Returns an object of type `BlockerResult`.
    391     ///
    392     /// # Arguments
    393     /// * `url` - The URL of the request to check
    394     /// * `source_url` - The URL from where the request is made
    395     /// * `request_type` - The resource type that the request points to
    396     pub fn check_network_urls(
    397         &self,
    398         url: &str,
    399         source_url: &str,
    400         request_type: &str,
    401     ) -> BlockerResult {
    402         match Request::new(url, source_url, request_type) {
    403             Ok(request) => {
    404                 let blocker_result = self.engine.check_network_request(&request);
    405                 blocker_result.into()
    406             }
    407             Err(_) => BlockerResult {
    408                 matched: false,
    409                 important: false,
    410                 redirect: None,
    411                 rewritten_url: None,
    412                 exception: None,
    413                 filter: None,
    414             }
    415         }
    416     }
    417 
    418     /// Check if a request should be blocked based on the given parameters.
    419     ///
    420     /// # Arguments
    421     /// * `url` - The URL of the request to check
    422     /// * `hostname` - The given `url`'s hostname
    423     /// * `source_hostname` - The hostname of the source URL.
    424     /// * `request_type` - The resource type that the request points to
    425     /// * `third_party_request` - Is the given request to a third-party? Here,
    426     ///   `None` can be given and the engine will figure it out based on the
    427     ///   `hostname` and `source_hostname`.
    428     #[pyo3(signature = (url, hostname, source_hostname, request_type, third_party_request = None))]
    429     pub fn check_network_urls_with_hostnames(
    430         &self,
    431         url: &str,
    432         hostname: &str,
    433         source_hostname: &str,
    434         request_type: &str,
    435         third_party_request: Option<bool>,
    436     ) -> BlockerResult {
    437         let third_party = third_party_request.unwrap_or_else(|| {
    438             hostname != source_hostname
    439         });
    440         let request = Request::preparsed(url, hostname, source_hostname, request_type, third_party);
    441         let blocker_result = self.engine.check_network_request(&request);
    442         blocker_result.into()
    443     }
    444 
    445     /// Check if a request should be blocked based on the given parameters.
    446     ///
    447     /// # Arguments
    448     /// * `url` - The URL of the request to check
    449     /// * `hostname` - The given `url`'s hostname
    450     /// * `source_hostname` - The hostname of the source URL.
    451     /// * `request_type` - The resource type that the request points to
    452     /// * `third_party_request` - Is the given request to a third-party? Here,
    453     ///   `None` can be given and the engine will figure it out based on the
    454     ///   `hostname` and `source_hostname`.
    455     /// * `previously_matched_rule` - Return a match as long as there are no
    456     ///    exceptions
    457     /// * `force_check_exceptions` - Check exceptions even if no other rule matches
    458     #[pyo3(signature = (url, hostname, source_hostname, request_type, third_party_request = None, previously_matched_rule = false, force_check_exceptions = false))]
    459     #[allow(clippy::too_many_arguments)]
    460     pub fn check_network_urls_with_hostnames_subset(
    461         &self,
    462         url: &str,
    463         hostname: &str,
    464         source_hostname: &str,
    465         request_type: &str,
    466         third_party_request: Option<bool>,
    467         previously_matched_rule: bool,
    468         force_check_exceptions: bool,
    469     ) -> BlockerResult {
    470         let third_party = third_party_request.unwrap_or_else(|| {
    471             hostname != source_hostname
    472         });
    473         let request = Request::preparsed(url, hostname, source_hostname, request_type, third_party);
    474         let blocker_result = self.engine.check_network_request_subset(
    475             &request,
    476             previously_matched_rule,
    477             force_check_exceptions,
    478         );
    479         blocker_result.into()
    480     }
    481 
    482     /// Sets this engine's resources to additionally include `resource`.
    483     ///
    484     /// # Arguments
    485     /// * `name`: Represents the primary name of the resource, often a filename
    486     /// * `content_type`: How to interpret the resource data within `content`.
    487     ///   Use `"template"` if wanting to specify a template resource type.
    488     /// * `content`: The resource data, encoded using standard base64 configuration
    489     /// * `aliases`: List of aliases for the resource
    490     #[pyo3(signature = (name, content_type, content, aliases = None))]
    491     pub fn add_resource(
    492         &mut self,
    493         name: &str,
    494         content_type: &str,
    495         content: &str,
    496         aliases: Option<Vec<String>>,
    497     ) -> PyResult<()> {
    498         let resource = Resource {
    499             name: name.to_string(),
    500             aliases: aliases.unwrap_or_default(),
    501             kind: match content_type {
    502                 "template" => ResourceType::Template,
    503                 _ => ResourceType::Mime(MimeType::from(std::borrow::Cow::from(
    504                     content_type.to_string(),
    505                 ))),
    506             },
    507             content: content.to_string(),
    508             dependencies: Vec::new(),
    509             permission: PermissionMask::default(),
    510         };
    511 
    512         self.engine.use_resources(std::iter::once(resource));
    513         Ok(())
    514     }
    515 
    516     /// Serialize this blocking engine to bytes. They can then be deserialized
    517     /// using `deserialize()` to get the same engine again.
    518     pub fn serialize<'p>(&self, py: Python<'p>) -> PyResult<Bound<'p, PyBytes>> {
    519         let bytes = self.engine.serialize();
    520         let py_bytes = PyBytes::new_bound(py, &bytes);
    521         Ok(py_bytes)
    522     }
    523 
    524     /// Serialize this blocking engine to a file. The file can then be
    525     /// deserialized using `deserialize_from_file()` to get the same engine
    526     /// again.
    527     pub fn serialize_to_file(&self, file: &str) -> PyResult<()> {
    528         let data = self.engine.serialize();
    529         let mut fd = fs::OpenOptions::new()
    530             .create(true)
    531             .truncate(true)
    532             .write(true)
    533             .open(file)?;
    534         fd.write_all(&data)?;
    535         Ok(())
    536     }
    537 
    538     /// Deserialize a blocking engine from bytes produced with `serialize()`.
    539     pub fn deserialize(&mut self, serialized: &[u8]) -> PyResult<()> {
    540         self.engine.deserialize(serialized).map_err(|_| {
    541             DeserializationError::new_err("Failed to deserialize engine data")
    542         })
    543     }
    544 
    545     /// Deserialize a blocking engine from file produced with
    546     /// `serialize_to_file()`.
    547     pub fn deserialize_from_file(&mut self, file: &str) -> PyResult<()> {
    548         let mut fd = fs::File::open(file)?;
    549         let mut data: Vec<u8> = Vec::new();
    550         fd.read_to_end(&mut data)?;
    551         self.deserialize(&data)
    552     }
    553 
    554     /// Checks if the given filter exists in the blocking engine.
    555     /// Note: This method has been removed in the latest version of adblock-rust.
    556     /// It will always return false.
    557     pub fn filter_exists(&self, _filter: &str) -> bool {
    558         // This method no longer exists in the adblock-rust API
    559         false
    560     }
    561 
    562     /// Sets this engine's tags to be _only_ the ones provided in tags.
    563     ///
    564     /// Tags can be used to cheaply enable or disable network rules with a
    565     /// corresponding $tag option.
    566     pub fn use_tags(&mut self, tags: Vec<String>) {
    567         let tag_refs: Vec<&str> = tags.iter().map(|s| s.as_str()).collect();
    568         self.engine.use_tags(&tag_refs);
    569     }
    570 
    571     /// Sets this engine's tags to additionally include the ones provided in
    572     /// tags.
    573     ///
    574     /// Tags can be used to cheaply enable or disable network rules with a
    575     /// corresponding $tag option.
    576     pub fn enable_tags(&mut self, tags: Vec<String>) {
    577         let tag_refs: Vec<&str> = tags.iter().map(|s| s.as_str()).collect();
    578         self.engine.enable_tags(&tag_refs);
    579     }
    580 
    581     /// Sets this engine's tags to no longer include the ones provided in
    582     /// tags.
    583     ///
    584     /// Tags can be used to cheaply enable or disable network rules with a
    585     /// corresponding $tag option.
    586     pub fn disable_tags(&mut self, tags: Vec<String>) {
    587         let tag_refs: Vec<&str> = tags.iter().map(|s| s.as_str()).collect();
    588         self.engine.disable_tags(&tag_refs);
    589     }
    590 
    591     /// Checks if a given tag exists in this engine.
    592     ///
    593     /// Tags can be used to cheaply enable or disable network rules with a
    594     /// corresponding $tag option.
    595     pub fn tag_exists(&self, tag: &str) -> bool {
    596         self.engine.tag_exists(tag)
    597     }
    598 
    599     /// Returns a set of cosmetic filter resources required for a particular
    600     /// url. Once this has been called, all CSS ids and classes on a
    601     /// page should be passed to hidden_class_id_selectors to obtain any
    602     /// stylesheets consisting of generic rules.
    603     pub fn url_cosmetic_resources(&self, url: &str) -> UrlSpecificResources {
    604         self.engine.url_cosmetic_resources(url).into()
    605     }
    606 
    607     /// If any of the provided CSS classes or ids could cause a certain generic
    608     /// CSS hide rule (i.e. `{ display: none !important; }`) to be required, this
    609     /// method will return a list of CSS selectors corresponding to rules
    610     /// referencing those classes or ids, provided that the corresponding rules
    611     /// are not excepted.
    612     ///
    613     /// Exceptions should be passed directly from UrlSpecificResources.
    614     pub fn hidden_class_id_selectors(
    615         &self,
    616         classes: Vec<String>,
    617         ids: Vec<String>,
    618         exceptions: HashSet<String>,
    619     ) -> PyResult<Vec<String>> {
    620         Ok(self
    621             .engine
    622             .hidden_class_id_selectors(&classes, &ids, &exceptions))
    623     }
    624 
    625     fn __repr__(&self) -> PyResult<String> {
    626         Ok(format!(
    627             "Engine<optimize={}>",
    628             self.optimize.diy_python_repr()
    629         ))
    630     }
    631 }
    632 
    633 /// PyO3 doesn't offer the ability to get the Python representation of a Rust
    634 /// object, so we make our own trait.
    635 trait DiyPythonRepr {
    636     fn diy_python_repr(&self) -> String;
    637 }
    638 
    639 impl<T> DiyPythonRepr for Option<T>
    640 where
    641     T: DiyPythonRepr,
    642 {
    643     fn diy_python_repr(&self) -> String {
    644         match self {
    645             None => "None".to_owned(),
    646             Some(x) => x.diy_python_repr(),
    647         }
    648     }
    649 }
    650 
    651 impl DiyPythonRepr for String {
    652     fn diy_python_repr(&self) -> String {
    653         let mut res = format!("{:?}", self);
    654         // This is safe to do since we know that `res` will always be of
    655         // length >= 2.
    656         res.replace_range(0..1, "'");
    657         res.replace_range(res.len() - 1..res.len(), "'");
    658         res
    659     }
    660 }
    661 
    662 impl DiyPythonRepr for bool {
    663     fn diy_python_repr(&self) -> String {
    664         if *self {
    665             "True".to_owned()
    666         } else {
    667             "False".to_owned()
    668         }
    669     }
    670 }